From 1e44de35b3d722e46ed38e796d60b364af301c77 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Mon, 27 Aug 2018 16:53:44 +0800 Subject: [PATCH] MDL-63044 block_timeline: add timeline block --- .../tool/usertours/tests/behat/create_tour.feature | 8 +- .../amd/build/calendar_events_repository.min.js | 1 + blocks/timeline/amd/build/event_list.min.js | 1 + blocks/timeline/amd/build/main.min.js | 1 + blocks/timeline/amd/build/paging_bar.min.js | 1 + blocks/timeline/amd/build/paging_content.min.js | 1 + blocks/timeline/amd/build/view.min.js | 1 + blocks/timeline/amd/build/view_courses.min.js | 1 + blocks/timeline/amd/build/view_dates.min.js | 1 + blocks/timeline/amd/build/view_nav.min.js | 1 + .../timeline/amd/src/calendar_events_repository.js | 166 ++++++ blocks/timeline/amd/src/event_list.js | 465 ++++++++++++++++ blocks/timeline/amd/src/main.js | 57 ++ blocks/timeline/amd/src/view.js | 97 ++++ blocks/timeline/amd/src/view_courses.js | 606 +++++++++++++++++++++ blocks/timeline/amd/src/view_dates.js | 103 ++++ blocks/timeline/amd/src/view_nav.js | 120 ++++ blocks/timeline/block_timeline.php | 72 +++ blocks/timeline/classes/output/main.php | 81 +++ blocks/timeline/classes/output/renderer.php | 48 ++ blocks/timeline/classes/privacy/provider.php | 46 ++ blocks/timeline/db/access.php | 50 ++ blocks/timeline/db/install.php | 108 ++++ blocks/timeline/lang/en/block_timeline.php | 49 ++ blocks/timeline/pix/activities.svg | 41 ++ blocks/timeline/pix/courses.svg | 52 ++ .../course-item-loading-placeholder.mustache | 40 ++ blocks/timeline/templates/course-item.mustache | 36 ++ blocks/timeline/templates/course-items.mustache | 31 ++ .../timeline/templates/event-list-content.mustache | 71 +++ blocks/timeline/templates/event-list-item.mustache | 63 +++ .../timeline/templates/event-list-items.mustache | 70 +++ blocks/timeline/templates/event-list.mustache | 55 ++ blocks/timeline/templates/main.mustache | 54 ++ blocks/timeline/templates/nav-day-filter.mustache | 90 +++ .../timeline/templates/nav-view-selector.mustache | 51 ++ .../templates/placeholder-event-list-item.mustache | 42 ++ blocks/timeline/templates/view-courses.mustache | 49 ++ blocks/timeline/templates/view-dates.mustache | 27 + blocks/timeline/templates/view.mustache | 44 ++ .../tests/behat/block_timeline_courses.feature | 72 +++ .../tests/behat/block_timeline_dates.feature | 88 +++ blocks/timeline/version.php | 29 + blocks/upgrade.txt | 4 + lib/blocklib.php | 2 +- lib/classes/plugin_manager.php | 2 +- .../course-item-loading-placeholder.mustache | 44 ++ .../block_timeline/event-list-item.mustache | 63 +++ .../block_timeline/event-list-items.mustache | 70 +++ .../templates/block_timeline/event-list.mustache | 55 ++ .../templates/block_timeline/main.mustache | 50 ++ .../block_timeline/nav-day-filter.mustache | 67 +++ .../block_timeline/nav-view-selector.mustache | 46 ++ .../placeholder-event-list-item.mustache | 31 ++ .../templates/block_timeline/view.mustache | 44 ++ 55 files changed, 3562 insertions(+), 6 deletions(-) create mode 100644 blocks/timeline/amd/build/calendar_events_repository.min.js create mode 100644 blocks/timeline/amd/build/event_list.min.js create mode 100644 blocks/timeline/amd/build/main.min.js create mode 100644 blocks/timeline/amd/build/paging_bar.min.js create mode 100644 blocks/timeline/amd/build/paging_content.min.js create mode 100644 blocks/timeline/amd/build/view.min.js create mode 100644 blocks/timeline/amd/build/view_courses.min.js create mode 100644 blocks/timeline/amd/build/view_dates.min.js create mode 100644 blocks/timeline/amd/build/view_nav.min.js create mode 100644 blocks/timeline/amd/src/calendar_events_repository.js create mode 100644 blocks/timeline/amd/src/event_list.js create mode 100644 blocks/timeline/amd/src/main.js create mode 100644 blocks/timeline/amd/src/view.js create mode 100644 blocks/timeline/amd/src/view_courses.js create mode 100644 blocks/timeline/amd/src/view_dates.js create mode 100644 blocks/timeline/amd/src/view_nav.js create mode 100644 blocks/timeline/block_timeline.php create mode 100644 blocks/timeline/classes/output/main.php create mode 100644 blocks/timeline/classes/output/renderer.php create mode 100644 blocks/timeline/classes/privacy/provider.php create mode 100644 blocks/timeline/db/access.php create mode 100644 blocks/timeline/db/install.php create mode 100644 blocks/timeline/lang/en/block_timeline.php create mode 100644 blocks/timeline/pix/activities.svg create mode 100644 blocks/timeline/pix/courses.svg create mode 100644 blocks/timeline/templates/course-item-loading-placeholder.mustache create mode 100644 blocks/timeline/templates/course-item.mustache create mode 100644 blocks/timeline/templates/course-items.mustache create mode 100644 blocks/timeline/templates/event-list-content.mustache create mode 100644 blocks/timeline/templates/event-list-item.mustache create mode 100644 blocks/timeline/templates/event-list-items.mustache create mode 100644 blocks/timeline/templates/event-list.mustache create mode 100644 blocks/timeline/templates/main.mustache create mode 100644 blocks/timeline/templates/nav-day-filter.mustache create mode 100644 blocks/timeline/templates/nav-view-selector.mustache create mode 100644 blocks/timeline/templates/placeholder-event-list-item.mustache create mode 100644 blocks/timeline/templates/view-courses.mustache create mode 100644 blocks/timeline/templates/view-dates.mustache create mode 100644 blocks/timeline/templates/view.mustache create mode 100644 blocks/timeline/tests/behat/block_timeline_courses.feature create mode 100644 blocks/timeline/tests/behat/block_timeline_dates.feature create mode 100644 blocks/timeline/version.php create mode 100644 theme/bootstrapbase/templates/block_timeline/course-item-loading-placeholder.mustache create mode 100644 theme/bootstrapbase/templates/block_timeline/event-list-item.mustache create mode 100644 theme/bootstrapbase/templates/block_timeline/event-list-items.mustache create mode 100644 theme/bootstrapbase/templates/block_timeline/event-list.mustache create mode 100644 theme/bootstrapbase/templates/block_timeline/main.mustache create mode 100644 theme/bootstrapbase/templates/block_timeline/nav-day-filter.mustache create mode 100644 theme/bootstrapbase/templates/block_timeline/nav-view-selector.mustache create mode 100644 theme/bootstrapbase/templates/block_timeline/placeholder-event-list-item.mustache create mode 100644 theme/bootstrapbase/templates/block_timeline/view.mustache diff --git a/admin/tool/usertours/tests/behat/create_tour.feature b/admin/tool/usertours/tests/behat/create_tour.feature index 33c7db7cc77..09f99ff9cc0 100644 --- a/admin/tool/usertours/tests/behat/create_tour.feature +++ b/admin/tool/usertours/tests/behat/create_tour.feature @@ -27,16 +27,16 @@ Feature: Add a new user tour | Selector | .usermenu | User menu | This is your personal user menu. You'll find your personal preferences and your user profile here. | When I am on homepage Then I should see "Welcome to your personal learning space. We'd like to give you a quick tour to show you some of the areas you may find helpful" - And I press "Next" + And I click on "Next" "button" in the "[data-role='flexitour-step']" "css_element" And I should see "This area shows you what's happening in some of your courses" And I should not see "This is the Calendar. All of your assignments and due dates can be found here" - And I press "Next" + And I click on "Next" "button" in the "[data-role='flexitour-step']" "css_element" And I should see "This is the Calendar. All of your assignments and due dates can be found here" And I should not see "This area shows you what's happening in some of your courses" - And I press "Prev" + And I click on "Prev" "button" in the "[data-role='flexitour-step']" "css_element" And I should not see "This is the Calendar. All of your assignments and due dates can be found here" And I should see "This area shows you what's happening in some of your courses" - And I press "End tour" + And I click on "End tour" "button" in the "[data-role='flexitour-step']" "css_element" And I should not see "This area shows you what's happening in some of your courses" And I am on homepage And I should not see "Welcome to your personal learning space. We'd like to give you a quick tour to show you some of the areas you may find helpful" diff --git a/blocks/timeline/amd/build/calendar_events_repository.min.js b/blocks/timeline/amd/build/calendar_events_repository.min.js new file mode 100644 index 00000000000..5c6e35a1419 --- /dev/null +++ b/blocks/timeline/amd/build/calendar_events_repository.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax","core/notification"],function(a,b,c){var d=20,e=function(a){a.hasOwnProperty("limit")||(a.limit=d),a.limitnum=a.limit,delete a.limit,a.hasOwnProperty("starttime")&&(a.timesortfrom=a.starttime,delete a.starttime),a.hasOwnProperty("endtime")&&(a.timesortto=a.endtime,delete a.endtime);var e={methodname:"core_calendar_get_action_events_by_course",args:a},f=b.call([e])[0];return f.fail(c.exception),f},f=function(a){a.hasOwnProperty("limit")||(a.limit=10),a.limitnum=a.limit,delete a.limit,a.hasOwnProperty("starttime")&&(a.timesortfrom=a.starttime,delete a.starttime),a.hasOwnProperty("endtime")&&(a.timesortto=a.endtime,delete a.endtime);var d={methodname:"core_calendar_get_action_events_by_courses",args:a},e=b.call([d])[0];return e.fail(c.exception),e},g=function(a){a.hasOwnProperty("limit")||(a.limit=d),a.limitnum=a.limit,delete a.limit,a.hasOwnProperty("starttime")&&(a.timesortfrom=a.starttime,delete a.starttime),a.hasOwnProperty("endtime")&&(a.timesortto=a.endtime,delete a.endtime);var e={methodname:"core_calendar_get_action_events_by_timesort",args:a},f=b.call([e])[0];return f.fail(c.exception),f};return{queryByTime:g,queryByCourse:e,queryByCourses:f}}); \ No newline at end of file diff --git a/blocks/timeline/amd/build/event_list.min.js b/blocks/timeline/amd/build/event_list.min.js new file mode 100644 index 00000000000..cd209c581a0 --- /dev/null +++ b/blocks/timeline/amd/build/event_list.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/templates","core/paged_content_factory","core/str","core/user_date","block_timeline/calendar_events_repository"],function(a,b,c,d,e,f,g){var h=86400,i={EMPTY_MESSAGE:'[data-region="empty-message"]',ROOT:'[data-region="event-list-container"]',EVENT_LIST_CONTENT:'[data-region="event-list-content"]',EVENT_LIST_LOADING_PLACEHOLDER:'[data-region="event-list-loading-placeholder"]'},j={EVENT_LIST_CONTENT:"block_timeline/event-list-content"},k={ignoreControlWhileLoading:!0,controlPlacementBottom:!0,ariaLabels:{itemsperpagecomponents:"ariaeventlistpagelimit, block_timeline"}},l=function(a){a.find(i.EVENT_LIST_CONTENT).addClass("hidden"),a.find(i.EMPTY_MESSAGE).removeClass("hidden")},m=function(a){a.find(i.EVENT_LIST_CONTENT).removeClass("hidden"),a.find(i.EMPTY_MESSAGE).addClass("hidden")},n=function(a){a.find(i.EVENT_LIST_CONTENT).empty()},o=function(a,b){var c={},d={eventsbyday:[]};return a.forEach(function(a){var d=f.getUserMidnightForTimestamp(a.timesort,b);c[d]?c[d].push(a):c[d]=[a]}),Object.keys(c).forEach(function(a){var e=c[a];d.eventsbyday.push({past:a0},x=function(a){return parseInt(a.attr("data-offset"),10)},y=function(a,b){a.attr("data-offset",b)},z=function(a){return parseInt(a.attr("data-limit"),10)},A=function(a){return parseInt(a.attr("data-days-offset"),10)},B=function(a){var b=a.attr("data-days-limit");return void 0!=b?parseInt(b,10):void 0},C=function(a){return parseInt(a.attr("data-midnight"),10)},D=function(a){var b=C(a),c=A(a);return b+c*o},E=function(a){var b=C(a),c=B(a);return void 0!=c&&b+c*o},F=function(a,b,c,d){var e={courseids:a,starttime:b,limit:c};return d&&(e.endtime=d),h.queryByCourses(e)},G=function(a){return a.data("last-event-load-time")},H=function(a,b){a.data("last-event-load-time",b)},I=function(a,b){return G(a)>b},J=function(a,b,c){var d=a.map(function(a){return a.id});return F(d,b,m+1,c)},K=function(a,b,c,d,f,g){return e.render(j.COURSE_ITEMS,{courses:a,midnight:c,hasdaysoffset:!0,hasdayslimit:void 0!=f,daysoffset:d,dayslimit:f,nodayslimit:void 0==f,urls:{noevents:g}}).then(function(a){return p(b),a?v(b,a):w(b)||u(b),a}).then(function(c){return a.length. + +/** + * A javascript module to retrieve calendar events from the server. + * + * @module block_timeline/calendar_events_repository + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) { + + var DEFAULT_LIMIT = 20; + + /** + * Retrieve a list of calendar events for the logged in user for the + * given course. + * + * Valid args are: + * int courseid Only get events for this course + * int starttime Only get events after this time + * int endtime Only get events before this time + * int limit Limit the number of results returned + * int aftereventid Offset the result set from the given id + * + * @method queryByCourse + * @param {object} args The request arguments + * @return {promise} Resolved with an array of the calendar events + */ + var queryByCourse = function(args) { + if (!args.hasOwnProperty('limit')) { + args.limit = DEFAULT_LIMIT; + } + + args.limitnum = args.limit; + delete args.limit; + + if (args.hasOwnProperty('starttime')) { + args.timesortfrom = args.starttime; + delete args.starttime; + } + + if (args.hasOwnProperty('endtime')) { + args.timesortto = args.endtime; + delete args.endtime; + } + + var request = { + methodname: 'core_calendar_get_action_events_by_course', + args: args + }; + + var promise = Ajax.call([request])[0]; + + promise.fail(Notification.exception); + + return promise; + }; + + /** + * Retrieve a list of calendar events for the given courses for the + * logged in user. + * + * Valid args are: + * array courseids Get events for these courses + * int starttime Only get events after this time + * int endtime Only get events before this time + * int limit Limit the number of results returned + * + * @method queryByCourses + * @param {object} args The request arguments + * @return {promise} Resolved with an array of the calendar events + */ + var queryByCourses = function(args) { + if (!args.hasOwnProperty('limit')) { + // This is intentionally smaller than the default limit. + args.limit = 10; + } + + args.limitnum = args.limit; + delete args.limit; + + if (args.hasOwnProperty('starttime')) { + args.timesortfrom = args.starttime; + delete args.starttime; + } + + if (args.hasOwnProperty('endtime')) { + args.timesortto = args.endtime; + delete args.endtime; + } + + var request = { + methodname: 'core_calendar_get_action_events_by_courses', + args: args + }; + + var promise = Ajax.call([request])[0]; + + promise.fail(Notification.exception); + + return promise; + }; + + /** + * Retrieve a list of calendar events for the logged in user after the given + * time. + * + * Valid args are: + * int starttime Only get events after this time + * int endtime Only get events before this time + * int limit Limit the number of results returned + * int aftereventid Offset the result set from the given id + * + * @method queryByTime + * @param {object} args The request arguments + * @return {promise} Resolved with an array of the calendar events + */ + var queryByTime = function(args) { + if (!args.hasOwnProperty('limit')) { + args.limit = DEFAULT_LIMIT; + } + + args.limitnum = args.limit; + delete args.limit; + + if (args.hasOwnProperty('starttime')) { + args.timesortfrom = args.starttime; + delete args.starttime; + } + + if (args.hasOwnProperty('endtime')) { + args.timesortto = args.endtime; + delete args.endtime; + } + + var request = { + methodname: 'core_calendar_get_action_events_by_timesort', + args: args + }; + + var promise = Ajax.call([request])[0]; + + promise.fail(Notification.exception); + + return promise; + }; + + return { + queryByTime: queryByTime, + queryByCourse: queryByCourse, + queryByCourses: queryByCourses, + }; +}); diff --git a/blocks/timeline/amd/src/event_list.js b/blocks/timeline/amd/src/event_list.js new file mode 100644 index 00000000000..dfd1517a561 --- /dev/null +++ b/blocks/timeline/amd/src/event_list.js @@ -0,0 +1,465 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Javascript to load and render the list of calendar events for a + * given day range. + * + * @module block_timeline/event_list + * @package block_timeline + * @copyright 2016 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'core/notification', + 'core/templates', + 'core/paged_content_factory', + 'core/str', + 'core/user_date', + 'block_timeline/calendar_events_repository' +], +function( + $, + Notification, + Templates, + PagedContentFactory, + Str, + UserDate, + CalendarEventsRepository +) { + + var SECONDS_IN_DAY = 60 * 60 * 24; + + var SELECTORS = { + EMPTY_MESSAGE: '[data-region="empty-message"]', + ROOT: '[data-region="event-list-container"]', + EVENT_LIST_CONTENT: '[data-region="event-list-content"]', + EVENT_LIST_LOADING_PLACEHOLDER: '[data-region="event-list-loading-placeholder"]', + }; + + var TEMPLATES = { + EVENT_LIST_CONTENT: 'block_timeline/event-list-content' + }; + + // We want the paged content controls below the paged content area + // and the controls should be ignored while data is loading. + var DEFAULT_PAGED_CONTENT_CONFIG = { + ignoreControlWhileLoading: true, + controlPlacementBottom: true, + ariaLabels: { + itemsperpagecomponents: 'ariaeventlistpagelimit, block_timeline', + } + }; + + /** + * Hide the content area and display the empty content message. + * + * @param {object} root The container element + */ + var hideContent = function(root) { + root.find(SELECTORS.EVENT_LIST_CONTENT).addClass('hidden'); + root.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden'); + }; + + /** + * Show the content area and hide the empty content message. + * + * @param {object} root The container element + */ + var showContent = function(root) { + root.find(SELECTORS.EVENT_LIST_CONTENT).removeClass('hidden'); + root.find(SELECTORS.EMPTY_MESSAGE).addClass('hidden'); + }; + + /** + * Empty the content area. + * + * @param {object} root The container element + */ + var emptyContent = function(root) { + root.find(SELECTORS.EVENT_LIST_CONTENT).empty(); + }; + + /** + * Construct the template context from a list of calendar events. The events + * are grouped by which day they are on. The day is calculated from the user's + * midnight timestamp to ensure that the calculation is timezone agnostic. + * + * The return data structure will look like: + * { + * eventsbyday: [ + * { + * dayTimestamp: 1533744000, + * events: [ + * { ...event 1 data... }, + * { ...event 2 data... } + * ] + * }, + * { + * dayTimestamp: 1533830400, + * events: [ + * { ...event 3 data... }, + * { ...event 4 data... } + * ] + * } + * ] + * } + * + * Each day timestamp is the day's midnight in the user's timezone. + * + * @param {array} calendarEvents List of calendar events + * @param {Number} midnight A timestamp representing midnight in the user's timezone + * @return {object} + */ + var buildTemplateContext = function(calendarEvents, midnight) { + var eventsByDay = {}; + var templateContext = { + eventsbyday: [] + }; + + calendarEvents.forEach(function(calendarEvent) { + var dayTimestamp = UserDate.getUserMidnightForTimestamp(calendarEvent.timesort, midnight); + if (eventsByDay[dayTimestamp]) { + eventsByDay[dayTimestamp].push(calendarEvent); + } else { + eventsByDay[dayTimestamp] = [calendarEvent]; + } + }); + + Object.keys(eventsByDay).forEach(function(dayTimestamp) { + var events = eventsByDay[dayTimestamp]; + templateContext.eventsbyday.push({ + past: dayTimestamp < midnight, + dayTimestamp: dayTimestamp, + events: events + }); + }); + + return templateContext; + }; + + /** + * Render the HTML for the given calendar events. + * + * @param {array} calendarEvents A list of calendar events + * @param {Number} midnight A timestamp representing midnight for the user + * @return {promise} Resolved with HTML and JS strings. + */ + var render = function(calendarEvents, midnight) { + var templateContext = buildTemplateContext(calendarEvents, midnight); + var templateName = TEMPLATES.EVENT_LIST_CONTENT; + + return Templates.render(templateName, templateContext); + }; + + /** + * Retrieve a list of calendar events from the server for the given + * constraints. + * + * @param {Number} midnight The user's midnight time in unix timestamp. + * @param {Number} limit Limit the result set to this number of items + * @param {Number} daysOffset How many days (from midnight) to offset the results from + * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to + * @param {int|falsey} lastId The ID of the last seen event (if any) + * @param {int|undefined} courseId Course ID to restrict events to + * @return {promise} A jquery promise + */ + var load = function(midnight, limit, daysOffset, daysLimit, lastId, courseId) { + var startTime = midnight + (daysOffset * SECONDS_IN_DAY); + var endTime = daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : false; + + var args = { + starttime: startTime, + limit: limit, + }; + + if (lastId) { + args.aftereventid = lastId; + } + + if (endTime) { + args.endtime = endTime; + } + + if (courseId) { + // If we have a course id then we only want events from that course. + args.courseid = courseId; + return CalendarEventsRepository.queryByCourse(args); + } else { + // Otherwise we want events from any course. + return CalendarEventsRepository.queryByTime(args); + } + }; + + /** + * Handle a single page request from the paged content. Uses the given page data to request + * the events from the server. + * + * Checks the given preloadedPages before sending a request to the server to make sure we + * don't load data unnecessarily. + * + * @param {object} pageData A single page data (see core/paged_content_pages for more info). + * @param {object} actions Paged content actions (see core/paged_content_pages for more info). + * @param {Number} midnight The user's midnight time in unix timestamp. + * @param {object} lastIds The last event ID for each loaded page. Page number is key, id is value. + * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value. + * @param {int|undefined} courseId Course ID to restrict events to + * @param {Number} daysOffset How many days (from midnight) to offset the results from + * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to + * @return {object} jQuery promise resolved with calendar events. + */ + var loadEventsFromPageData = function( + pageData, + actions, + midnight, + lastIds, + preloadedPages, + courseId, + daysOffset, + daysLimit + ) { + var pageNumber = pageData.pageNumber; + var limit = pageData.limit; + var lastPageNumber = pageNumber; + + // This is here to protect us if, for some reason, the pages + // are loaded out of order somehow and we don't have a reference + // to the previous page. In that case, scan back to find the most + // recent page we've seen. + while (!lastIds.hasOwnProperty(lastPageNumber)) { + lastPageNumber--; + } + // Use the last id of the most recent page. + var lastId = lastIds[lastPageNumber]; + var eventsPromise = null; + + if (preloadedPages && preloadedPages.hasOwnProperty(pageNumber)) { + // This page has been preloaded so use that rather than load the values + // again. + eventsPromise = preloadedPages[pageNumber]; + } else { + // Load one more than the given limit so that we can tell if there + // is more content to load after this. + eventsPromise = load(midnight, limit + 1, daysOffset, daysLimit, lastId, courseId); + } + + return eventsPromise.then(function(result) { + if (!result.events.length) { + // If we didn't get any events back then tell the paged content + // that we're done loading. + actions.allItemsLoaded(pageNumber); + return []; + } + + var calendarEvents = result.events; + // We expect to receive limit + 1 events back from the server. + // Any less means there are no more events to load. + var loadedAll = calendarEvents.length <= limit; + + if (loadedAll) { + // Tell the pagination that everything is loaded. + actions.allItemsLoaded(pageNumber); + } else { + // Remove the last element from the array because it isn't + // needed in this result set. + calendarEvents.pop(); + } + + return calendarEvents; + }); + }; + + /** + * Use the paged content factory to create a paged content element for showing + * the event list. We only provide a page limit to the factory because we don't + * know exactly how many pages we'll need. This creates a paging bar with just + * next/previous buttons. + * + * This function specifies the callback for loading the event data that the user + * is requesting. + * + * @param {int|array} pageLimit A single limit or list of limits as options for the paged content + * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value. + * @param {Number} midnight The user's midnight time in unix timestamp. + * @param {object} firstLoad A jQuery promise to be resolved after the first set of data is loaded. + * @param {int|undefined} courseId Course ID to restrict events to + * @param {Number} daysOffset How many days (from midnight) to offset the results from + * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to + * @param {string} paginationAriaLabel String to set as the aria label for the pagination bar. + * @return {object} jQuery promise. + */ + var createPagedContent = function( + pageLimit, + preloadedPages, + midnight, + firstLoad, + courseId, + daysOffset, + daysLimit, + paginationAriaLabel + ) { + // Remember the last event id we loaded on each page because we can't + // use the offset value since the backend can skip events if the user doesn't + // have the capability to see them. Instead we load the next page of events + // based on the last seen event id. + var lastIds = {'1': 0}; + var hasContent = false; + var config = $.extend({}, DEFAULT_PAGED_CONTENT_CONFIG); + + return Str.get_string( + 'ariaeventlistpagelimit', + 'block_timeline', + $.isArray(pageLimit) ? pageLimit[0] : pageLimit + ) + .then(function(string) { + config.ariaLabels.itemsperpage = string; + config.ariaLabels.paginationnav = paginationAriaLabel; + return string; + }) + .then(function() { + return PagedContentFactory.createWithLimit( + pageLimit, + function(pagesData, actions) { + var promises = []; + + pagesData.forEach(function(pageData) { + var pageNumber = pageData.pageNumber; + // Load the page data. + var pagePromise = loadEventsFromPageData( + pageData, + actions, + midnight, + lastIds, + preloadedPages, + courseId, + daysOffset, + daysLimit + ).then(function(calendarEvents) { + if (calendarEvents.length) { + // Remember that we've loaded content. + hasContent = true; + // Remember the last id we've seen. + var lastEventId = calendarEvents[calendarEvents.length - 1].id; + // Record the id that the next page will need to start from. + lastIds[pageNumber + 1] = lastEventId; + // Get the HTML and JS for these calendar events. + return render(calendarEvents, midnight); + } else { + return calendarEvents; + } + }) + .catch(Notification.exception); + + promises.push(pagePromise); + }); + + $.when.apply($, promises).then(function() { + // Tell the calling code that the first page has been loaded + // and whether it contains any content. + firstLoad.resolve(hasContent); + return; + }) + .catch(function() { + firstLoad.resolve(hasContent); + }); + + return promises; + }, + config + ); + }); + }; + + /** + * Create a paged content region for the calendar events in the given root element. + * The content of the root element are replaced with a new paged content section + * each time this function is called. + * + * This function will be called each time the offset or limit values are changed to + * reload the event list region. + * + * @param {object} root The event list container element + * @param {int|array} pageLimit A single limit or list of limits as options for the paged content + * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value. + * @param {string} paginationAriaLabel String to set as the aria label for the pagination bar. + */ + var init = function(root, pageLimit, preloadedPages, paginationAriaLabel) { + root = $(root); + + // Create a promise that will be resolved once the first set of page + // data has been loaded. This ensures that the loading placeholder isn't + // hidden until we have all of the data back to prevent the page elements + // jumping around. + var firstLoad = $.Deferred(); + var eventListContent = root.find(SELECTORS.EVENT_LIST_CONTENT); + var loadingPlaceholder = root.find(SELECTORS.EVENT_LIST_LOADING_PLACEHOLDER); + var courseId = root.attr('data-course-id'); + var daysOffset = parseInt(root.attr('data-days-offset'), 10); + var daysLimit = root.attr('data-days-limit'); + var midnight = parseInt(root.attr('data-midnight'), 10); + + // Make sure the content area and loading placeholder is visible. + // This is because the init function can be called to re-initialise + // an existing event list area. + emptyContent(root); + showContent(root); + loadingPlaceholder.removeClass('hidden'); + + // Days limit isn't mandatory. + if (daysLimit != undefined) { + daysLimit = parseInt(daysLimit, 10); + } + + // Created the paged content element. + createPagedContent(pageLimit, preloadedPages, midnight, firstLoad, courseId, daysOffset, daysLimit, paginationAriaLabel) + .then(function(html, js) { + html = $(html); + // Hide the content for now. + html.addClass('hidden'); + // Replace existing elements with the newly created paged content. + // If we're reinitialising an existing event list this will replace + // the old event list (including removing any event handlers). + Templates.replaceNodeContents(eventListContent, html, js); + + firstLoad.then(function(hasContent) { + // Prevent changing page elements too much by only showing the content + // once we've loaded some data for the first time. This allows our + // fancy loading placeholder to shine. + html.removeClass('hidden'); + loadingPlaceholder.addClass('hidden'); + + if (!hasContent) { + // If we didn't get any data then show the empty data message. + hideContent(root); + } + + return hasContent; + }) + .catch(function() { + return false; + }); + + return html; + }) + .catch(Notification.exception); + }; + + return { + init: init, + rootSelector: SELECTORS.ROOT, + }; +}); diff --git a/blocks/timeline/amd/src/main.js b/blocks/timeline/amd/src/main.js new file mode 100644 index 00000000000..334248b7f49 --- /dev/null +++ b/blocks/timeline/amd/src/main.js @@ -0,0 +1,57 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Javascript to initialise the timeline block. + * + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define( +[ + 'jquery', + 'block_timeline/view_nav', + 'block_timeline/view' +], +function( + $, + ViewNav, + View +) { + + var SELECTORS = { + TIMELINE_VIEW: '[data-region="timeline-view"]' + }; + + /** + * Initialise all of the modules for the timeline block. + * + * @param {object} root The root element for the timeline block. + */ + var init = function(root) { + root = $(root); + var viewRoot = root.find(SELECTORS.TIMELINE_VIEW); + + // Initialise the timeline navigation elements. + ViewNav.init(root, viewRoot); + // Initialise the timeline view modules. + View.init(viewRoot); + }; + + return { + init: init + }; +}); diff --git a/blocks/timeline/amd/src/view.js b/blocks/timeline/amd/src/view.js new file mode 100644 index 00000000000..89430a77fe5 --- /dev/null +++ b/blocks/timeline/amd/src/view.js @@ -0,0 +1,97 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Manage the timeline view for the timeline block. + * + * @package block_timeline + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define( +[ + 'jquery', + 'block_timeline/view_dates', + 'block_timeline/view_courses', +], +function( + $, + ViewDates, + ViewCourses +) { + + var SELECTORS = { + TIMELINE_DATES_VIEW: '[data-region="view-dates"]', + TIMELINE_COURSES_VIEW: '[data-region="view-courses"]', + }; + + /** + * Intialise the timeline dates and courses views on page load. + * This function should only be called once per page load because + * it can cause event listeners to be added to the page. + * + * @param {object} root The root element for the timeline view. + */ + var init = function(root) { + root = $(root); + var datesViewRoot = root.find(SELECTORS.TIMELINE_DATES_VIEW); + var coursesViewRoot = root.find(SELECTORS.TIMELINE_COURSES_VIEW); + + ViewDates.init(datesViewRoot); + ViewCourses.init(coursesViewRoot); + }; + + /** + * Reset the timeline dates and courses views to their original + * state on first page load. + * + * This is called when configuration has changed for the event lists + * to cause them to reload their data. + * + * @param {object} root The root element for the timeline view. + */ + var reset = function(root) { + var datesViewRoot = root.find(SELECTORS.TIMELINE_DATES_VIEW); + var coursesViewRoot = root.find(SELECTORS.TIMELINE_COURSES_VIEW); + ViewDates.reset(datesViewRoot); + ViewCourses.reset(coursesViewRoot); + }; + + /** + * Tell the timeline dates or courses view that it has been displayed. + * + * This is called each time one of the views is displayed and is used to + * lazy load the data within it on first load. + * + * @param {object} root The root element for the timeline view. + */ + var shown = function(root) { + var datesViewRoot = root.find(SELECTORS.TIMELINE_DATES_VIEW); + var coursesViewRoot = root.find(SELECTORS.TIMELINE_COURSES_VIEW); + + if (datesViewRoot.hasClass('active')) { + ViewDates.shown(datesViewRoot); + } else { + ViewCourses.shown(coursesViewRoot); + } + }; + + return { + init: init, + reset: reset, + shown: shown, + }; +}); diff --git a/blocks/timeline/amd/src/view_courses.js b/blocks/timeline/amd/src/view_courses.js new file mode 100644 index 00000000000..4749c920351 --- /dev/null +++ b/blocks/timeline/amd/src/view_courses.js @@ -0,0 +1,606 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Manage the timeline courses view for the timeline block. + * + * @package block_timeline + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define( +[ + 'jquery', + 'core/notification', + 'core/custom_interaction_events', + 'core/str', + 'core/templates', + 'block_timeline/event_list', + 'core_course/repository', + 'block_timeline/calendar_events_repository' +], +function( + $, + Notification, + CustomEvents, + Str, + Templates, + EventList, + CourseRepository, + EventsRepository +) { + + var SELECTORS = { + MORE_COURSES_BUTTON: '[data-action="more-courses"]', + MORE_COURSES_BUTTON_CONTAINER: '[data-region="more-courses-button-container"]', + NO_COURSES_EMPTY_MESSAGE: '[data-region="no-courses-empty-message"]', + COURSES_LIST: '[data-region="courses-list"]', + COURSE_ITEMS_LOADING_PLACEHOLDER: '[data-region="course-items-loading-placeholder"]', + COURSE_EVENTS_CONTAINER: '[data-region="course-events-container"]', + COURSE_NAME: '[data-region="course-name"]', + LOADING_ICON: '.loading-icon' + }; + + var TEMPLATES = { + COURSE_ITEMS: 'block_timeline/course-items', + LOADING_ICON: 'core/loading' + }; + + var COURSE_CLASSIFICATION = 'inprogress'; + var COURSE_SORT = 'fullname asc'; + var COURSE_EVENT_LIMIT = 5; + var COURSE_LIMIT = 2; + var SECONDS_IN_DAY = 60 * 60 * 24; + + /** + * Hide the loading placeholder elements. + * + * @param {object} root The rool element. + */ + var hideLoadingPlaceholder = function(root) { + root.find(SELECTORS.COURSE_ITEMS_LOADING_PLACEHOLDER).addClass('hidden'); + }; + + /** + * Hide the "more courses" button. + * + * @param {object} root The rool element. + */ + var hideMoreCoursesButton = function(root) { + root.find(SELECTORS.MORE_COURSES_BUTTON_CONTAINER).addClass('hidden'); + }; + + /** + * Show the "more courses" button. + * + * @param {object} root The rool element. + */ + var showMoreCoursesButton = function(root) { + root.find(SELECTORS.MORE_COURSES_BUTTON_CONTAINER).removeClass('hidden'); + }; + + /** + * Disable the "more courses" button and show the loading spinner. + * + * @param {object} root The rool element. + */ + var enableMoreCoursesButtonLoading = function(root) { + var button = root.find(SELECTORS.MORE_COURSES_BUTTON); + button.prop('disabled', true); + Templates.render(TEMPLATES.LOADING_ICON, {}) + .then(function(html) { + button.append(html); + return html; + }) + .catch(function() { + // It's not important if this false so just do so silently. + return false; + }); + }; + + /** + * Enable the "more courses" button and remove the loading spinner. + * + * @param {object} root The rool element. + */ + var disableMoreCoursesButtonLoading = function(root) { + var button = root.find(SELECTORS.MORE_COURSES_BUTTON); + button.prop('disabled', false); + button.find(SELECTORS.LOADING_ICON).remove(); + }; + + /** + * Display the message for when there are no courses available. + * + * @param {object} root The rool element. + */ + var showNoCoursesEmptyMessage = function(root) { + root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).removeClass('hidden'); + }; + + /** + * Render the course items HTML to the page. + * + * @param {object} root The rool element. + * @param {string} html The course items HTML to render. + */ + var renderCourseItemsHTML = function(root, html) { + var container = root.find(SELECTORS.COURSES_LIST); + Templates.appendNodeContents(container, html, ''); + }; + + /** + * Check if any courses have been loaded. + * + * @param {object} root The rool element. + * @return {bool} + */ + var hasLoadedCourses = function(root) { + return root.find(SELECTORS.COURSE_EVENTS_CONTAINER).length > 0; + }; + + /** + * Return the offset value for fetching courses. + * + * @param {object} root The rool element. + * @return {Number} + */ + var getOffset = function(root) { + return parseInt(root.attr('data-offset'), 10); + }; + + /** + * Set the offset value for fetching courses. + * + * @param {object} root The rool element. + * @param {Number} offset Offset value. + */ + var setOffset = function(root, offset) { + root.attr('data-offset', offset); + }; + + /** + * Return the limit value for fetching courses. + * + * @param {object} root The rool element. + * @return {Number} + */ + var getLimit = function(root) { + return parseInt(root.attr('data-limit'), 10); + }; + + /** + * Return the days offset value for fetching events. + * + * @param {object} root The rool element. + * @return {Number} + */ + var getDaysOffset = function(root) { + return parseInt(root.attr('data-days-offset'), 10); + }; + + /** + * Return the days limit value for fetching events. The days + * limit is optional so undefined will be returned if it isn't + * set. + * + * @param {object} root The rool element. + * @return {int|undefined} + */ + var getDaysLimit = function(root) { + var daysLimit = root.attr('data-days-limit'); + return daysLimit != undefined ? parseInt(daysLimit, 10) : undefined; + }; + + /** + * Return the timestamp for the user's midnight. + * + * @param {object} root The rool element. + * @return {Number} + */ + var getMidnight = function(root) { + return parseInt(root.attr('data-midnight'), 10); + }; + + /** + * Return the start time for fetching events. This is calculated + * based on the user's midnight value so that timezones are + * preserved. + * + * @param {object} root The rool element. + * @return {Number} + */ + var getStartTime = function(root) { + var midnight = getMidnight(root); + var daysOffset = getDaysOffset(root); + return midnight + (daysOffset * SECONDS_IN_DAY); + }; + + /** + * Return the end time for fetching events. This is calculated + * based on the user's midnight value so that timezones are + * preserved. + * + * @param {object} root The rool element. + * @return {Number} + */ + var getEndTime = function(root) { + var midnight = getMidnight(root); + var daysLimit = getDaysLimit(root); + return daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : false; + }; + + /** + * Get a list of events for the given course ids. Returns a promise that will + * be resolved with the events. + * + * @param {array} courseIds The list of course ids to fetch events for. + * @param {Number} startTime Timestamp to fetch events from. + * @param {Number} limit Limit to the number of events (this applies per course, not total) + * @param {Number} endTime Timestamp to fetch events to. + * @return {object} jQuery promise. + */ + var getEventsForCourseIds = function(courseIds, startTime, limit, endTime) { + var args = { + courseids: courseIds, + starttime: startTime, + limit: limit + }; + + if (endTime) { + args.endtime = endTime; + } + + return EventsRepository.queryByCourses(args); + }; + + /** + * Get the last time the events were reloaded. + * + * @param {object} root The rool element. + * @return {Number} + */ + var getEventReloadTime = function(root) { + return root.data('last-event-load-time'); + }; + + /** + * Set the last time the events were reloaded. + * + * @param {object} root The rool element. + * @param {Number} time Timestamp in milliseconds. + */ + var setEventReloadTime = function(root, time) { + root.data('last-event-load-time', time); + }; + + /** + * Check if events have begun reloading since the given + * time. + * + * @param {object} root The rool element. + * @param {Number} time Timestamp in milliseconds. + * @return {bool} + */ + var hasReloadedEventsSince = function(root, time) { + return getEventReloadTime(root) > time; + }; + + /** + * Send a request to the server to load the events for the courses. + * + * @param {array} courses List of course objects. + * @param {Number} startTime Timestamp to load events after. + * @param {int|undefined} endTime Timestamp to load events up until. + * @return {object} jQuery promise resolved with the events. + */ + var loadEventsForCourses = function(courses, startTime, endTime) { + var courseIds = courses.map(function(course) { + return course.id; + }); + + return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime); + }; + + /** + * Render the courses in the DOM once the server has returned the courses. + * + * @param {array} courses List of course objects. + * @param {object} root The root element + * @param {Number} midnight The midnight timestamp in the user's timezone. + * @param {Number} daysOffset Number of days from today to offset the events. + * @param {Number} daysLimit Number of days from today to limit the events to. + * @param {string} noEventsURL URL for the image to display for no events. + * @return {object} jQuery promise resolved after rendering is complete. + */ + var updateDisplayFromCourses = function(courses, root, midnight, daysOffset, daysLimit, noEventsURL) { + // Render the courses template. + return Templates.render(TEMPLATES.COURSE_ITEMS, { + courses: courses, + midnight: midnight, + hasdaysoffset: true, + hasdayslimit: daysLimit != undefined, + daysoffset: daysOffset, + dayslimit: daysLimit, + nodayslimit: daysLimit == undefined, + urls: { + noevents: noEventsURL + } + }).then(function(html) { + hideLoadingPlaceholder(root); + + if (html) { + // Template rendering is complete and we have the HTML so we can + // add it to the DOM. + renderCourseItemsHTML(root, html); + } else { + if (!hasLoadedCourses(root)) { + // There were no courses to render so show the empty placeholder + // message for the user to tell them. + showNoCoursesEmptyMessage(root); + } + } + + return html; + }) + .then(function(html) { + if (courses.length < COURSE_LIMIT) { + // We know there aren't any more courses because we got back less + // than we asked for so hide the button to request more. + hideMoreCoursesButton(root); + } else { + // Make sure the button is visible if there are more courses to load. + showMoreCoursesButton(root); + } + + return html; + }) + .catch(function() { + hideLoadingPlaceholder(root); + }); + }; + + /** + * Find all of the visible course blocks and initialise the event + * list module to being loading the events for the course block. + * + * @param {object} root The root element for the timeline courses view. + * @return {object} jQuery promise resolved with courses and events. + */ + var loadMoreCourses = function(root) { + var offset = getOffset(root); + var limit = getLimit(root); + + // Start loading the next set of courses. + return CourseRepository.getEnrolledCoursesByTimelineClassification( + COURSE_CLASSIFICATION, + limit, + offset, + COURSE_SORT + ).then(function(result) { + var startEventLoadingTime = Date.now(); + var courses = result.courses; + var nextOffset = result.nextoffset; + var daysOffset = getDaysOffset(root); + var daysLimit = getDaysLimit(root); + var midnight = getMidnight(root); + var startTime = getStartTime(root); + var endTime = getEndTime(root); + var noEventsURL = root.attr('data-no-events-url'); + // Record the next offset if we want to request more courses. + setOffset(root, nextOffset); + // Load the events for these courses. + var eventsPromise = loadEventsForCourses(courses, startTime, endTime); + // Render the courses in the DOM. + var renderPromise = updateDisplayFromCourses(courses, root, midnight, daysOffset, daysLimit, noEventsURL); + + return $.when(eventsPromise, renderPromise) + .then(function(eventsByCourse) { + if (hasReloadedEventsSince(root, startEventLoadingTime)) { + // All of the events are being reloaded so ignore our results. + return eventsByCourse; + } + + // When we've got all of the courses and events we can render the events in the + // correct course event list. + courses.forEach(function(course) { + var courseId = course.id; + var events = []; + var containerSelector = '[data-region="course-events-container"][data-course-id="' + courseId + '"]'; + var courseEventsContainer = root.find(containerSelector); + var eventListRoot = courseEventsContainer.find(EventList.rootSelector); + var courseGroups = eventsByCourse.groupedbycourse.filter(function(group) { + return group.courseid == courseId; + }); + + if (courseGroups.length) { + // Get the events for this course. + events = courseGroups[0].events; + } + + // Create a preloaded page to pass to the event list because we've already + // loaded the first page of events. + var pageOnePreload = $.Deferred().resolve({events: events}).promise(); + // Initialise the event list pagination area for this course. + Str.get_string('ariaeventlistpaginationnavcourses', 'block_timeline', course.fullnamedisplay) + .then(function(string) { + EventList.init(eventListRoot, COURSE_EVENT_LIMIT, {'1': pageOnePreload}, string); + return string; + }) + .catch(function() { + // An error is ok, just render with the default string. + EventList.init(eventListRoot, COURSE_EVENT_LIMIT, {'1': pageOnePreload}); + }); + }); + + return eventsByCourse; + }); + }).catch(Notification.exception); + }; + + /** + * Reload the events for all of the visible courses. These events will be loaded + * in a single request to the server. + * + * @param {object} root The root element. + * @return {object} jQuery promise resolved with courses and events. + */ + var reloadCourseEvents = function(root) { + var startReloadTime = Date.now(); + var startTime = getStartTime(root); + var endTime = getEndTime(root); + var courseEventsContainers = root.find(SELECTORS.COURSE_EVENTS_CONTAINER); + var courseIds = courseEventsContainers.map(function() { + return $(this).attr('data-course-id'); + }).get(); + + // Record when we started our request. + setEventReloadTime(root, startReloadTime); + + // Load all of the events for the given courses. + return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime) + .then(function(eventsByCourse) { + if (hasReloadedEventsSince(root, startReloadTime)) { + // A new reload has begun so ignore our results. + return eventsByCourse; + } + + courseEventsContainers.each(function(index, container) { + container = $(container); + var courseId = container.attr('data-course-id'); + var courseName = container.find(SELECTORS.COURSE_NAME).text(); + var eventListContainer = container.find(EventList.rootSelector); + var pageDeferred = $.Deferred(); + var events = []; + var courseGroups = eventsByCourse.groupedbycourse.filter(function(group) { + return group.courseid == courseId; + }); + + if (courseGroups.length) { + // Get the events just for this course. + events = courseGroups[0].events; + } + + pageDeferred.resolve({events: events}); + + // Re-initialise the events list with the preloaded events we just got from + // the server. + Str.get_string('ariaeventlistpaginationnavcourses', 'block_timeline', courseName) + .then(function(string) { + EventList.init(eventListContainer, COURSE_EVENT_LIMIT, {'1': pageDeferred.promise()}, string); + return string; + }) + .catch(function() { + // Ignore a failure to load the string. Just render with the default string. + EventList.init(eventListContainer, COURSE_EVENT_LIMIT, {'1': pageDeferred.promise()}); + }); + }); + + return eventsByCourse; + }).catch(Notification.exception); + }; + + /** + * Add event listeners to load more courses for the courses view. + * + * @param {object} root The root element for the timeline courses view. + */ + var registerEventListeners = function(root) { + CustomEvents.define(root, [CustomEvents.events.activate]); + // Show more courses and load their events when the user clicks the "more courses" + // button. + root.on(CustomEvents.events.activate, SELECTORS.MORE_COURSES_BUTTON, function(e, data) { + enableMoreCoursesButtonLoading(root); + loadMoreCourses(root) + .then(function() { + disableMoreCoursesButtonLoading(root); + return; + }) + .catch(function() { + disableMoreCoursesButtonLoading(root); + }); + + if (data) { + data.originalEvent.preventDefault(); + data.originalEvent.stopPropagation(); + } + e.stopPropagation(); + }); + }; + + /** + * Initialise the timeline courses view. Begin loading the events + * if this view is active. Add the relevant event listeners. + * + * This function should only be called once per page load because it + * is adding event listeners to the page. + * + * @param {object} root The root element for the timeline courses view. + */ + var init = function(root) { + root = $(root); + + setEventReloadTime(root, Date.now()); + + if (root.hasClass('active')) { + // Only load if this is active otherwise it will be lazy loaded later. + loadMoreCourses(root); + root.attr('data-seen', true); + } + + registerEventListeners(root); + }; + + /** + * Reset the element back to it's initial state. Begin loading the events again + * if this view is active. + * + * @param {object} root The root element for the timeline courses view. + */ + var reset = function(root) { + root.removeAttr('data-seen'); + if (root.hasClass('active')) { + shown(root); + } + }; + + /** + * If this is the first time this view has been displayed then begin loading + * the events. + * + * @param {object} root The root element for the timeline courses view. + */ + var shown = function(root) { + if (!root.attr('data-seen')) { + if (hasLoadedCourses(root)) { + // This isn't the first time this view is shown so just reload the + // events for the courses we've already loaded. + reloadCourseEvents(root); + } else { + // We haven't loaded any courses yet so do that now. + loadMoreCourses(root); + } + + root.attr('data-seen', true); + } + }; + + return { + init: init, + reset: reset, + shown: shown + }; +}); diff --git a/blocks/timeline/amd/src/view_dates.js b/blocks/timeline/amd/src/view_dates.js new file mode 100644 index 00000000000..b18d21a3fd3 --- /dev/null +++ b/blocks/timeline/amd/src/view_dates.js @@ -0,0 +1,103 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Manage the timeline dates view for the timeline block. + * + * @package block_timeline + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define( +[ + 'jquery', + 'core/str', + 'block_timeline/event_list' +], +function( + $, + Str, + EventList +) { + + var SELECTORS = { + EVENT_LIST_CONTAINER: '[data-region="event-list-container"]', + }; + + /** + * Initialise the event list and being loading the events. + * + * @param {object} root The root element for the timeline dates view. + */ + var load = function(root) { + var eventListContainer = root.find(SELECTORS.EVENT_LIST_CONTAINER); + Str.get_string('ariaeventlistpaginationnavdates', 'block_timeline') + .then(function(string) { + EventList.init(eventListContainer, [5, 10, 25], {}, string); + return string; + }) + .catch(function() { + // Ignore if we can't load the string. Still init the event list. + EventList.init(eventListContainer, [5, 10, 25]); + }); + }; + + /** + * Initialise the timeline dates view. Begin loading the events + * if this view is active. + * + * @param {object} root The root element for the timeline courses view. + */ + var init = function(root) { + root = $(root); + if (root.hasClass('active')) { + load(root); + root.attr('data-seen', true); + } + }; + + /** + * Reset the view back to it's initial state. If this view is active then + * beging loading the events. + * + * @param {object} root The root element for the timeline courses view. + */ + var reset = function(root) { + root.removeAttr('data-seen'); + if (root.hasClass('active')) { + load(root); + root.attr('data-seen', true); + } + }; + + /** + * Load the events if this is the first time the view is displayed. + * + * @param {object} root The root element for the timeline courses view. + */ + var shown = function(root) { + if (!root.attr('data-seen')) { + load(root); + root.attr('data-seen', true); + } + }; + + return { + init: init, + reset: reset, + shown: shown + }; +}); diff --git a/blocks/timeline/amd/src/view_nav.js b/blocks/timeline/amd/src/view_nav.js new file mode 100644 index 00000000000..092dcbd93fe --- /dev/null +++ b/blocks/timeline/amd/src/view_nav.js @@ -0,0 +1,120 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Manage the timeline view navigation for the timeline block. + * + * @package block_timeline + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define( +[ + 'jquery', + 'core/custom_interaction_events', + 'block_timeline/view' +], +function( + $, + CustomEvents, + View +) { + + var SELECTORS = { + TIMELINE_DAY_FILTER: '[data-region="day-filter"]', + TIMELINE_DAY_FILTER_OPTION: '[data-from]', + TIMELINE_VIEW_SELECTOR: '[data-region="view-selector"]', + DATA_DAYS_OFFSET: '[data-days-offset]', + DATA_DAYS_LIMIT: '[data-days-limit]', + }; + + /** + * Event listener for the day selector ("Next 7 days", "Next 30 days", etc). + * + * @param {object} root The root element for the timeline block + * @param {object} timelineViewRoot The root element for the timeline view + */ + var registerTimelineDaySelector = function(root, timelineViewRoot) { + var timelineDaySelectorContainer = root.find(SELECTORS.TIMELINE_DAY_FILTER); + + CustomEvents.define(timelineDaySelectorContainer, [CustomEvents.events.activate]); + timelineDaySelectorContainer.on( + CustomEvents.events.activate, + SELECTORS.TIMELINE_DAY_FILTER_OPTION, + function(e, data) { + var option = $(e.target).closest(SELECTORS.TIMELINE_DAY_FILTER_OPTION); + + if (option.hasClass('active')) { + // If it's already active then we don't need to do anything. + return; + } + + var daysOffset = option.attr('data-from'); + var daysLimit = option.attr('data-to'); + var elementsWithDaysOffset = root.find(SELECTORS.DATA_DAYS_OFFSET); + + elementsWithDaysOffset.attr('data-days-offset', daysOffset); + + if (daysLimit != undefined) { + elementsWithDaysOffset.attr('data-days-limit', daysLimit); + } else { + elementsWithDaysOffset.removeAttr('data-days-limit'); + } + + // Reset the views to reinitialise the event lists now that we've + // updated the day limits. + View.reset(timelineViewRoot); + + data.originalEvent.preventDefault(); + } + ); + }; + + /** + * Event listener for the "sort" button in the timeline navigation that allows for + * changing between the timeline dates and courses views. + * + * On a view change we tell the timeline view module that the view has been shown + * so that it can handle how to display the appropriate view. + * + * @param {object} root The root element for the timeline block + * @param {object} timelineViewRoot The root element for the timeline view + */ + var registerViewSelector = function(root, timelineViewRoot) { + // Listen for when the user changes tab so that we can show the first set of courses + // and load their events when they request the sort by courses view for the first time. + root.find(SELECTORS.TIMELINE_VIEW_SELECTOR).on('shown shown.bs.tab', function() { + View.shown(timelineViewRoot); + }); + }; + + /** + * Initialise the timeline view navigation by adding event listeners to + * the navigation elements. + * + * @param {object} root The root element for the timeline block + * @param {object} timelineViewRoot The root element for the timeline view + */ + var init = function(root, timelineViewRoot) { + root = $(root); + registerTimelineDaySelector(root, timelineViewRoot); + registerViewSelector(root, timelineViewRoot); + }; + + return { + init: init + }; +}); diff --git a/blocks/timeline/block_timeline.php b/blocks/timeline/block_timeline.php new file mode 100644 index 00000000000..5a24f9ca98f --- /dev/null +++ b/blocks/timeline/block_timeline.php @@ -0,0 +1,72 @@ +. + +/** + * Contains the class for the Timeline block. + * + * @package block_timeline + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Timeline block class. + * + * @package block_timeline + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_timeline extends block_base { + + /** + * Init. + */ + public function init() { + $this->title = get_string('pluginname', 'block_timeline'); + } + + /** + * Returns the contents. + * + * @return stdClass contents of block + */ + public function get_content() { + if (isset($this->content)) { + return $this->content; + } + + $renderable = new \block_timeline\output\main(); + $renderer = $this->page->get_renderer('block_timeline'); + + $this->content = (object) [ + 'text' => $renderer->render($renderable), + 'footer' => '' + ]; + + return $this->content; + } + + /** + * Locations where block can be displayed. + * + * @return array + */ + public function applicable_formats() { + return array('my' => true); + } +} diff --git a/blocks/timeline/classes/output/main.php b/blocks/timeline/classes/output/main.php new file mode 100644 index 00000000000..b5e96097b22 --- /dev/null +++ b/blocks/timeline/classes/output/main.php @@ -0,0 +1,81 @@ +. + +/** + * Class containing data for timeline block. + * + * @package block_timeline + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace block_timeline\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use renderer_base; +use templatable; +use core_course\external\course_summary_exporter; + +require_once($CFG->dirroot . '/course/lib.php'); +require_once($CFG->libdir . '/completionlib.php'); + +/** + * Class containing data for timeline block. + * + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class main implements renderable, templatable { + + /** Number of courses to load per page */ + const COURSES_PER_PAGE = 2; + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param \renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + + $nocoursesurl = $output->image_url('courses', 'block_timeline')->out(); + $noeventsurl = $output->image_url('activities', 'block_timeline')->out(); + + $requiredproperties = course_summary_exporter::define_properties(); + $fields = join(',', array_keys($requiredproperties)); + $courses = course_get_enrolled_courses_for_logged_in_user(0, 0, null, $fields); + list($inprogresscourses, $processedcount) = course_filter_courses_by_timeline_classification( + $courses, + COURSE_TIMELINE_INPROGRESS, + self::COURSES_PER_PAGE + ); + $formattedcourses = array_map(function($course) use ($output) { + \context_helper::preload_from_record($course); + $context = \context_course::instance($course->id); + $exporter = new course_summary_exporter($course, ['context' => $context]); + return $exporter->export($output); + }, $inprogresscourses); + + return [ + 'midnight' => usergetmidnight(time()), + 'coursepages' => [$formattedcourses], + 'urls' => [ + 'nocourses' => $nocoursesurl, + 'noevents' => $noeventsurl + ] + ]; + } +} diff --git a/blocks/timeline/classes/output/renderer.php b/blocks/timeline/classes/output/renderer.php new file mode 100644 index 00000000000..dbe6bb73c73 --- /dev/null +++ b/blocks/timeline/classes/output/renderer.php @@ -0,0 +1,48 @@ +. + +/** + * Timeline block rendrer. + * + * @package block_timeline + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace block_timeline\output; +defined('MOODLE_INTERNAL') || die; + +use plugin_renderer_base; +use renderable; + +/** + * Timeline block renderer. + * + * @package block_timeline + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + + /** + * Return the main content for the block timeline. + * + * @param main $main The main renderable + * @return string HTML string + */ + public function render_main(main $main) { + return $this->render_from_template('block_timeline/main', $main->export_for_template($this)); + } +} diff --git a/blocks/timeline/classes/privacy/provider.php b/blocks/timeline/classes/privacy/provider.php new file mode 100644 index 00000000000..02ae526ba74 --- /dev/null +++ b/blocks/timeline/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for block_timeline. + * + * @package block_timeline + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_timeline\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for block_timeline. + * + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/blocks/timeline/db/access.php b/blocks/timeline/db/access.php new file mode 100644 index 00000000000..6bd70a287cf --- /dev/null +++ b/blocks/timeline/db/access.php @@ -0,0 +1,50 @@ +. + +/** + * Capabilities for the timeline block. + * + * @package block_timeline + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'block/timeline:myaddinstance' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => array( + 'user' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/my:manageblocks' + ), + + 'block/timeline:addinstance' => array( + 'riskbitmask' => RISK_SPAM | RISK_XSS, + + 'captype' => 'write', + 'contextlevel' => CONTEXT_BLOCK, + 'archetypes' => array( + 'manager' => CAP_ALLOW + ), + + 'clonepermissionsfrom' => 'moodle/site:manageblocks' + ) +); diff --git a/blocks/timeline/db/install.php b/blocks/timeline/db/install.php new file mode 100644 index 00000000000..c5a3d658de3 --- /dev/null +++ b/blocks/timeline/db/install.php @@ -0,0 +1,108 @@ +. + +/** + * Timeline block installation. + * + * @package block_timeline + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + /** + * Add the timeline block to the dashboard for all users by default + * when it is installed. + */ +function xmldb_block_timeline_install() { + global $DB; + + if ($DB->count_records('block_instances') < 1) { + // Only add the timeline block if it's being installed on an existing site. + // For new sites it will be added by blocks_add_default_system_blocks(). + return; + } + + if ($defaultmypage = $DB->get_record('my_pages', array('userid' => null, 'name' => '__default', 'private' => 1))) { + $subpagepattern = $defaultmypage->id; + } else { + $subpagepattern = null; + } + + $page = new moodle_page(); + $systemcontext = context_system::instance(); + $page->set_context($systemcontext); + // Add the block to the default /my. + $page->blocks->add_region(BLOCK_POS_RIGHT); + $page->blocks->add_block('timeline', BLOCK_POS_RIGHT, 0, false, 'my-index', $subpagepattern); + + // Now we need to find all users that have viewed their dashboard because it'll have + // made duplicates of the default block_instances for them so they won't see the new + // timeline block without the admin resetting all of the dashboards. + // + // Instead we'll just add the timeline block to their dashboards here. We will only + // add the timeline block if they still have the myoverview block. + $sql = "SELECT parentcontextid, subpagepattern + FROM {block_instances} + WHERE pagetypepattern = 'my-index' + AND blockname = 'myoverview' + AND parentcontextid != ?"; + $params = [$systemcontext->id]; + $existingrecords = $DB->get_recordset_sql($sql, $params); + $blockinstances = []; + $seencontexts = []; + $now = time(); + + foreach ($existingrecords as $existingrecord) { + $parentcontextid = $existingrecord->parentcontextid; + if (isset($seencontexts[$parentcontextid])) { + // If we've seen this context already then skip it because we don't want + // to add duplicate timeline blocks to the same context. This happens + // if something funny is going on with the subpagepattern. + continue; + } else { + $seencontexts[$parentcontextid] = true; + } + + $blockinstances[] = [ + 'blockname' => 'timeline', + 'parentcontextid' => $parentcontextid, + 'showinsubcontexts' => false, + 'pagetypepattern' => 'my-index', + 'subpagepattern' => $existingrecord->subpagepattern, + 'defaultregion' => BLOCK_POS_RIGHT, + 'defaultweight' => 0, + 'configdata' => '', + 'timecreated' => $now, + 'timemodified' => $now, + ]; + + if (count($blockinstances) >= 1000) { + // Insert after every 1000 records so that the memory usage doesn't + // get out of control. + $DB->insert_records('block_instances', $blockinstances); + $blockinstances = []; + } + } + + $existingrecords->close(); + + if (!empty($blockinstances)) { + // Insert what ever is left over. + $DB->insert_records('block_instances', $blockinstances); + } +} diff --git a/blocks/timeline/lang/en/block_timeline.php b/blocks/timeline/lang/en/block_timeline.php new file mode 100644 index 00000000000..70f961ce203 --- /dev/null +++ b/blocks/timeline/lang/en/block_timeline.php @@ -0,0 +1,49 @@ +. + +/** + * Lang strings for the timeline block. + * + * @package block_timeline + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['ariadayfilter'] = 'Filter timeline items'; +$string['ariadayfilteroption'] = '{$a} filter option'; +$string['ariaeventlistitem'] = '{$a->name} activity in {$a->course} is due on {$a->date}'; +$string['ariaeventlistpagelimit'] = 'Show {$a} activities per page'; +$string['ariaeventlistpaginationnavdates'] = 'Timeline activities pagination'; +$string['ariaeventlistpaginationnavcourses'] = 'Timeline activities for course {$a} pagination'; +$string['ariaviewselector'] = 'Sort timeline items'; +$string['ariaviewselectoroption'] = '{$a} sort option'; +$string['duedate'] = 'Due date'; +$string['morecourses'] = 'More courses'; +$string['timeline:addinstance'] = 'Add a new timeline block'; +$string['timeline:myaddinstance'] = 'Add a new timeline block to Dashboard'; +$string['nocoursesinprogress'] = 'No in progress courses'; +$string['noevents'] = 'No upcoming activities due'; +$string['next30days'] = 'Next 30 days'; +$string['next7days'] = 'Next 7 days'; +$string['next3months'] = 'Next 3 months'; +$string['next6months'] = 'Next 6 months'; +$string['overdue'] = 'Overdue'; +$string['pluginname'] = 'Timeline'; +$string['sortbycourses'] = 'Sort by courses'; +$string['sortbydates'] = 'Sort by dates'; +$string['timeline'] = 'Timeline'; +$string['viewcourse'] = 'View course'; +$string['privacy:metadata'] = 'The timeline block does not store any personal data.'; diff --git a/blocks/timeline/pix/activities.svg b/blocks/timeline/pix/activities.svg new file mode 100644 index 00000000000..ed7546ade4d --- /dev/null +++ b/blocks/timeline/pix/activities.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/blocks/timeline/pix/courses.svg b/blocks/timeline/pix/courses.svg new file mode 100644 index 00000000000..75e59fcf04b --- /dev/null +++ b/blocks/timeline/pix/courses.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/blocks/timeline/templates/course-item-loading-placeholder.mustache b/blocks/timeline/templates/course-item-loading-placeholder.mustache new file mode 100644 index 00000000000..d2c8168125b --- /dev/null +++ b/blocks/timeline/templates/course-item-loading-placeholder.mustache @@ -0,0 +1,40 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/course-item-loading-placeholder + + This template renders the each course block containing a summary and calendar events. + + Example context (json): + {} +}} +
  • +
    +
    +
      + {{> block_timeline/placeholder-event-list-item }} + {{> block_timeline/placeholder-event-list-item }} + {{> block_timeline/placeholder-event-list-item }} + {{> block_timeline/placeholder-event-list-item }} + {{> block_timeline/placeholder-event-list-item }} +
    +
    +
    +
    +
    +
    +
  • diff --git a/blocks/timeline/templates/course-item.mustache b/blocks/timeline/templates/course-item.mustache new file mode 100644 index 00000000000..dafb42a691e --- /dev/null +++ b/blocks/timeline/templates/course-item.mustache @@ -0,0 +1,36 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/course-item + + This template renders the each course block containing a summary and calendar events. + + Example context (json): + { + "shortname": "course 3", + "viewurl": "https://www.google.com", + "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout." + } +}} +
  • +
    +

    {{{fullnamedisplay}}}

    + {{< block_timeline/event-list }} + {{$courseid}}{{id}}{{/courseid}} + {{/ block_timeline/event-list }} +
    +
  • diff --git a/blocks/timeline/templates/course-items.mustache b/blocks/timeline/templates/course-items.mustache new file mode 100644 index 00000000000..444a13019e4 --- /dev/null +++ b/blocks/timeline/templates/course-items.mustache @@ -0,0 +1,31 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/course-items + + This template renders the each course block containing a summary and calendar events. + + Example context (json): + { + "shortname": "course 3", + "viewurl": "https://www.google.com", + "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout." + } +}} +{{#courses}} + {{> block_timeline/course-item }} +{{/courses}} diff --git a/blocks/timeline/templates/event-list-content.mustache b/blocks/timeline/templates/event-list-content.mustache new file mode 100644 index 00000000000..b8df729d6d8 --- /dev/null +++ b/blocks/timeline/templates/event-list-content.mustache @@ -0,0 +1,71 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/event-list-content + + This template renders a group of event list items for the timeline block. + + Example context (json): + { + "events": [ + { + "name": "Assignment due 1", + "url": "https://www.google.com", + "timesort": 1490320388, + "course": { + "fullnamedisplay": "Course 1" + }, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + }, + { + "name": "Assignment due 2", + "url": "https://www.google.com", + "timesort": 1490320388, + "course": { + "fullnamedisplay": "Course 1" + }, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + } + ] + } +}} +
    + {{#eventsbyday}} +
    {{#userdate}} {{dayTimestamp}}, {{#str}} strftimedayshort, core_langconfig {{/str}} {{/userdate}}
    + {{> block_timeline/event-list-items }} + {{/eventsbyday}} +
    \ No newline at end of file diff --git a/blocks/timeline/templates/event-list-item.mustache b/blocks/timeline/templates/event-list-item.mustache new file mode 100644 index 00000000000..40fe0dccf4e --- /dev/null +++ b/blocks/timeline/templates/event-list-item.mustache @@ -0,0 +1,63 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/event-list-item + + This template renders an event list item for the timeline block. + + Example context (json): + { + "name": "Assignment due 1", + "url": "https://www.google.com", + "timesort": 1490320388, + "course": { + "fullnamedisplay": "Course 1" + }, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "showitemcount": true, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + } +}} + +
    +
    + {{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}} +
    +
    +
    {{{name}}}
    + {{{course.fullnamedisplay}}} +
    + + {{#userdate}} {{timesort}}, {{#str}} strftimetime24, core_langconfig {{/str}} {{/userdate}} + +
    +
    diff --git a/blocks/timeline/templates/event-list-items.mustache b/blocks/timeline/templates/event-list-items.mustache new file mode 100644 index 00000000000..27f6b424a62 --- /dev/null +++ b/blocks/timeline/templates/event-list-items.mustache @@ -0,0 +1,70 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/event-list-items + + This template renders a group of event list items for the timeline block. + + Example context (json): + { + "events": [ + { + "name": "Assignment due 1", + "url": "https://www.google.com", + "timesort": 1490320388, + "course": { + "fullnamedisplay": "Course 1" + }, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + }, + { + "name": "Assignment due 2", + "url": "https://www.google.com", + "timesort": 1490320388, + "course": { + "fullnamedisplay": "Course 1" + }, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + } + ] + } +}} +
    +{{#events}} + {{> block_timeline/event-list-item }} +{{/events}} +
    diff --git a/blocks/timeline/templates/event-list.mustache b/blocks/timeline/templates/event-list.mustache new file mode 100644 index 00000000000..9a640c496ac --- /dev/null +++ b/blocks/timeline/templates/event-list.mustache @@ -0,0 +1,55 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/event-list + + This template renders a list of events for the timeline block. + + Example context (json): + { + } +}} +
    +
    +
      + {{> block_timeline/placeholder-event-list-item }} + {{> block_timeline/placeholder-event-list-item }} + {{> block_timeline/placeholder-event-list-item }} + {{> block_timeline/placeholder-event-list-item }} + {{> block_timeline/placeholder-event-list-item }} +
    +
    +
    +
    +
    +
    +
    + +
    diff --git a/blocks/timeline/templates/main.mustache b/blocks/timeline/templates/main.mustache new file mode 100644 index 00000000000..021cf479717 --- /dev/null +++ b/blocks/timeline/templates/main.mustache @@ -0,0 +1,54 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/main + + This template renders the main content area for the timeline block. + + Example context (json): + {} +}} + +
    +
    +
    +
    + {{> block_timeline/nav-day-filter }} +
    +
    + {{> block_timeline/nav-view-selector }} +
    +
    +
    +
    + {{> block_timeline/view }} +
    +
    +{{#js}} +require( +[ + 'jquery', + 'block_timeline/main', +], +function( + $, + Main +) { + var root = $('#block-timeline-{{uniqid}}'); + Main.init(root); +}); +{{/js}} diff --git a/blocks/timeline/templates/nav-day-filter.mustache b/blocks/timeline/templates/nav-day-filter.mustache new file mode 100644 index 00000000000..90afd627173 --- /dev/null +++ b/blocks/timeline/templates/nav-day-filter.mustache @@ -0,0 +1,90 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/nav-day-filter + + This template renders the day range selector for the timeline view. + + Example context (json): + {} +}} + diff --git a/blocks/timeline/templates/nav-view-selector.mustache b/blocks/timeline/templates/nav-view-selector.mustache new file mode 100644 index 00000000000..50c2add83d7 --- /dev/null +++ b/blocks/timeline/templates/nav-view-selector.mustache @@ -0,0 +1,51 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/nav-view-selector + + This template renders the timeline sort selector. + + Example context (json): + {} +}} +
    + + +
    diff --git a/blocks/timeline/templates/placeholder-event-list-item.mustache b/blocks/timeline/templates/placeholder-event-list-item.mustache new file mode 100644 index 00000000000..ad01fc0a015 --- /dev/null +++ b/blocks/timeline/templates/placeholder-event-list-item.mustache @@ -0,0 +1,42 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/event-list-item + + This template renders an event list item loading placeholder for the timeline block. + + Example context (json): + {} +}} +
  • +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
  • diff --git a/blocks/timeline/templates/view-courses.mustache b/blocks/timeline/templates/view-courses.mustache new file mode 100644 index 00000000000..d4d1799da11 --- /dev/null +++ b/blocks/timeline/templates/view-courses.mustache @@ -0,0 +1,49 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/view-courses + + This template renders the timeline view by courses for the timeline block. + + Example context (json): + {} +}} +
    +
      + {{> block_timeline/course-item-loading-placeholder }} + {{> block_timeline/course-item-loading-placeholder }} +
    +
    +
    +
      + + diff --git a/blocks/timeline/templates/view-dates.mustache b/blocks/timeline/templates/view-dates.mustache new file mode 100644 index 00000000000..0b571f0c0e4 --- /dev/null +++ b/blocks/timeline/templates/view-dates.mustache @@ -0,0 +1,27 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/view-dates + + This template renders the timeline view by dates for the timeline block. + + Example context (json): + {} +}} +
      + {{> block_timeline/event-list }} +
      diff --git a/blocks/timeline/templates/view.mustache b/blocks/timeline/templates/view.mustache new file mode 100644 index 00000000000..73deceeb915 --- /dev/null +++ b/blocks/timeline/templates/view.mustache @@ -0,0 +1,44 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/view + + This template renders the timeline view for the timeline block. + + Example context (json): + {} +}} +
      +
      +
      + {{> block_timeline/view-dates }} +
      +
      + {{> block_timeline/view-courses }} +
      +
      +
      \ No newline at end of file diff --git a/blocks/timeline/tests/behat/block_timeline_courses.feature b/blocks/timeline/tests/behat/block_timeline_courses.feature new file mode 100644 index 00000000000..f9376847a4c --- /dev/null +++ b/blocks/timeline/tests/behat/block_timeline_courses.feature @@ -0,0 +1,72 @@ +@block @block_timeline @javascript +Feature: The timeline block allows users to see upcoming activities + In order to enable the timeline block + As a student + I can add the timeline block to my dashboard + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + And the following "courses" exist: + | fullname | shortname | category | startdate | enddate | + | Course 1 | C1 | 0 | ##yesterday## | ##tomorrow## | + | Course 2 | C2 | 0 | ##yesterday## | ##tomorrow## | + | Course 3 | C3 | 0 | ##yesterday## | ##tomorrow## | + | Course 4 | C4 | 0 | ##first day of next month## | ##last day of next month## | + And the following "activities" exist: + | activity | course | idnumber | name | intro | timeopen | timeclose | + | choice | C2 | choice1 | Test choice 1 | Test choice description | ##yesterday## | ##tomorrow## | + | choice | C1 | choice2 | Test choice 2 | Test choice description | ##1 month ago## | ##15 days ago## | + | choice | C3 | choice3 | Test choice 3 | Test choice description | ##first day of +5 months## | ##last day of +5 months## | + | feedback | C2 | feedback1 | Test feedback 1 | Test feedback description | ##yesterday## | ##tomorrow## | + | feedback | C1 | feedback2 | Test feedback 2 | Test feedback description | ##first day of +10 months## | ##last day of +10 months## | + | feedback | C3 | feedback3 | Test feedback 3 | Test feedback description | ##first day of +5 months## | ##last day of +5 months## | + | feedback | C4 | feedback4 | Test feedback 4 | Test feedback description | ##yesterday## | ##tomorrow## | + And the following "activities" exist: + | activity | course | idnumber | name | intro | timeopen | duedate | + | assign | C1 | assign1 | Test assign 1 | Test assign description | ##1 month ago## | ##yesterday## | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | student1 | C2 | student | + | student1 | C3 | student | + | student1 | C4 | student | + + Scenario: Next 30 days in course view + Given I log in as "student1" + And I click on "Sort" "button" in the "Timeline" "block" + When I click on "Sort by courses" "link" in the "Timeline" "block" + Then I should see "Course 1" in the "Timeline" "block" + And I should see "Course 2" in the "Timeline" "block" + And I should see "More courses" in the "Timeline" "block" + And I should see "Test choice 1 closes" in the "Timeline" "block" + And I should see "Test feedback 1 closes" in the "Timeline" "block" + And I should not see "Course 3" in the "Timeline" "block" + And I should not see "Test choice 2 closes" in the "Timeline" "block" + And I should not see "Test choice 3 closes" in the "Timeline" "block" + And I should not see "Test feedback 2 closes" in the "Timeline" "block" + And I should not see "Test feedback 3 closes" in the "Timeline" "block" + And I should not see "Test assign 1 is due" in the "Timeline" "block" + + Scenario: All in course view + Given I log in as "student1" + And I click on "Next 30 days" "button" in the "Timeline" "block" + And I click on "All" "link" in the "Timeline" "block" + And I click on "Sort" "button" in the "Timeline" "block" + And I click on "Sort by courses" "link" in the "Timeline" "block" + When I click on "More courses" "button" in the "Timeline" "block" + Then I should see "Course 3" in the "Timeline" "block" + And I should see "Course 2" in the "Timeline" "block" + And I should see "Course 1" in the "Timeline" "block" + And I should see "Test choice 1 closes" in the "Timeline" "block" + And I should see "Test choice 3 closes" in the "Timeline" "block" + And I should see "Test feedback 1 closes" in the "Timeline" "block" + And I should see "Test feedback 2 closes" in the "Timeline" "block" + And I should see "Test feedback 3 closes" in the "Timeline" "block" + And I should see "Test assign 1 is due" in the "Timeline" "block" + And I should not see "More courses" in the "Timeline" "block" + And I should not see "Course 4" in the "Timeline" "block" + And I should not see "Test choice 2 closes" in the "Timeline" "block" + And I should not see "Test feedback 4 closes" in the "Timeline" "block" diff --git a/blocks/timeline/tests/behat/block_timeline_dates.feature b/blocks/timeline/tests/behat/block_timeline_dates.feature new file mode 100644 index 00000000000..6aaf269f6b0 --- /dev/null +++ b/blocks/timeline/tests/behat/block_timeline_dates.feature @@ -0,0 +1,88 @@ +@block @block_timeline @javascript +Feature: The timeline block allows users to see upcoming activities + In order to enable the timeline block + As a student + I can add the timeline block to my dashboard + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + And the following "courses" exist: + | fullname | shortname | category | startdate | enddate | + | Course 1 | C1 | 0 | ##1 month ago## | ##15 days ago## | + | Course 2 | C2 | 0 | ##yesterday## | ##tomorrow## | + | Course 3 | C3 | 0 | ##first day of next month## | ##last day of next month## | + And the following "activities" exist: + | activity | course | idnumber | name | intro | timeopen | timeclose | + | choice | C2 | choice1 | Test choice 1 | Test choice description | ##yesterday## | ##tomorrow## | + | choice | C1 | choice2 | Test choice 2 | Test choice description | ##1 month ago## | ##15 days ago## | + | choice | C3 | choice3 | Test choice 3 | Test choice description | ##first day of +5 months## | ##last day of +5 months## | + | feedback | C2 | feedback1 | Test feedback 1 | Test feedback description | ##yesterday## | ##tomorrow## | + | feedback | C1 | feedback2 | Test feedback 2 | Test feedback description | ##first day of +10 months## | ##last day of +10 months## | + | feedback | C3 | feedback3 | Test feedback 3 | Test feedback description | ##first day of +5 months## | ##last day of +5 months## | + And the following "activities" exist: + | activity | course | idnumber | name | intro | timeopen | duedate | + | assign | C1 | assign1 | Test assign 1 | Test assign description | ##1 month ago## | ##yesterday## | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | student1 | C2 | student | + | student1 | C3 | student | + + Scenario: Next 7 days in date view + Given I log in as "student1" + And I click on "Next 30 days" "button" in the "Timeline" "block" + When I click on "Next 7 days" "link" in the "Timeline" "block" + Then I should see "Test choice 1 closes" in the "Timeline" "block" + And I should see "Test feedback 1 closes" in the "Timeline" "block" + And I should not see "Test choice 2 closes" in the "Timeline" "block" + And I should not see "Test choice 3 closes" in the "Timeline" "block" + And I should not see "Test feedback 3 closes" in the "Timeline" "block" + And I should not see "Test assign 1 is due" in the "Timeline" "block" + + Scenario: Overdue in date view + Given I log in as "student1" + And I click on "Next 30 days" "button" in the "Timeline" "block" + When I click on "Overdue" "link" in the "Timeline" "block" + Then I should see "Test assign 1 is due" in the "Timeline" "block" + And I should not see "Test choice 2 closes" in the "Timeline" "block" + And I should not see "Test feedback 1 closes" in the "Timeline" "block" + And I should not see "Test choice 1 closes" in the "Timeline" "block" + And I should not see "Test choice 3 closes" in the "Timeline" "block" + And I should not see "Test feedback 3 closes" in the "Timeline" "block" + + Scenario: All in date view + Given I log in as "student1" + And I click on "Next 30 days" "button" in the "Timeline" "block" + When I click on "All" "link" in the "Timeline" "block" + Then I should see "Test assign 1 is due" in the "Timeline" "block" + And I should see "Test feedback 1 closes" in the "Timeline" "block" + And I should see "Test choice 1 closes" in the "Timeline" "block" + And I should see "Test choice 3 closes" in the "Timeline" "block" + And I should see "Test feedback 3 closes" in the "Timeline" "block" + And I should not see "Test choice 2 closes" in the "Timeline" "block" + And I should not see "Test feedback 2 closes" in the "Timeline" "block" + And I click on "[data-region='paging-bar'] [data-control='next']" "css_element" in the "Timeline" "block" + And I should see "Test feedback 2 closes" in the "Timeline" "block" + And I should not see "Test assign 1 is due" in the "Timeline" "block" + And I should not see "Test feedback 1 closes" in the "Timeline" "block" + And I should not see "Test choice 1 closes" in the "Timeline" "block" + And I should not see "Test choice 3 closes" in the "Timeline" "block" + And I should not see "Test feedback 3 closes" in the "Timeline" "block" + And I should not see "Test choice 2 closes" in the "Timeline" "block" + + Scenario: All in date view no next + Given I log in as "student1" + And I click on "Next 30 days" "button" in the "Timeline" "block" + And I click on "All" "link" in the "Timeline" "block" + And I click on "5" "button" in the "Timeline" "block" + When I click on "25" "link" in the "Timeline" "block" + Then I should see "Test assign 1 is due" in the "Timeline" "block" + And I should see "Test feedback 1 closes" in the "Timeline" "block" + And I should see "Test choice 1 closes" in the "Timeline" "block" + And I should see "Test choice 3 closes" in the "Timeline" "block" + And I should see "Test feedback 3 closes" in the "Timeline" "block" + And I should see "Test feedback 2 closes" in the "Timeline" "block" + And I should not see "Test choice 2 closes" in the "Timeline" "block" diff --git a/blocks/timeline/version.php b/blocks/timeline/version.php new file mode 100644 index 00000000000..64de7f35a1f --- /dev/null +++ b/blocks/timeline/version.php @@ -0,0 +1,29 @@ +. + +/** + * Version details for the timeline block. + * + * @package block_timeline + * @copyright 2018 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2018083100; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2018082400; // Requires this Moodle version. +$plugin->component = 'block_timeline'; // Full name of the plugin (used for diagnostics). diff --git a/blocks/upgrade.txt b/blocks/upgrade.txt index 7ef8f0abd0c..1234de4f591 100644 --- a/blocks/upgrade.txt +++ b/blocks/upgrade.txt @@ -1,6 +1,10 @@ This files describes API changes in /blocks/* - activity modules, information provided here is intended especially for developers. +=== 3.6 === + +* The timeline view from block_myoverview has been split out into block_timeline. + === 3.4 === * The block_instances table now contains fields timecreated and timemodified. If third-party code diff --git a/lib/blocklib.php b/lib/blocklib.php index c8fa2455a08..bf42e6c06b6 100644 --- a/lib/blocklib.php +++ b/lib/blocklib.php @@ -2583,7 +2583,7 @@ function blocks_add_default_system_blocks() { $subpagepattern = null; } - $newblocks = array('private_files', 'online_users', 'badges', 'calendar_month', 'calendar_upcoming'); + $newblocks = array('timeline', 'private_files', 'online_users', 'badges', 'calendar_month', 'calendar_upcoming'); $newcontent = array('lp', 'myoverview'); $page->blocks->add_blocks(array(BLOCK_POS_RIGHT => $newblocks, 'content' => $newcontent), 'my-index', $subpagepattern); } diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index e319dc3613e..ca16fd4b8f8 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1721,7 +1721,7 @@ class core_plugin_manager { 'private_files', 'quiz_results', 'recent_activity', 'rss_client', 'search_forums', 'section_links', 'selfcompletion', 'settings', 'site_main_menu', - 'social_activities', 'tag_flickr', 'tag_youtube', 'tags' + 'social_activities', 'tag_flickr', 'tag_youtube', 'tags', 'timeline' ), 'booktool' => array( diff --git a/theme/bootstrapbase/templates/block_timeline/course-item-loading-placeholder.mustache b/theme/bootstrapbase/templates/block_timeline/course-item-loading-placeholder.mustache new file mode 100644 index 00000000000..096ec11a5c5 --- /dev/null +++ b/theme/bootstrapbase/templates/block_timeline/course-item-loading-placeholder.mustache @@ -0,0 +1,44 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/course-item-loading-placeholder + + This template renders the each course block containing a summary and calendar events. + + Example context (json): + { + "shortname": "course 3", + "viewurl": "https://www.google.com", + "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout." + } +}} +
    • +
      +
      +
        + {{> block_timeline/placeholder-event-list-item }} + {{> block_timeline/placeholder-event-list-item }} + {{> block_timeline/placeholder-event-list-item }} + {{> block_timeline/placeholder-event-list-item }} + {{> block_timeline/placeholder-event-list-item }} +
      +
      +
      +
      +
      +
      +
    • diff --git a/theme/bootstrapbase/templates/block_timeline/event-list-item.mustache b/theme/bootstrapbase/templates/block_timeline/event-list-item.mustache new file mode 100644 index 00000000000..af008b17919 --- /dev/null +++ b/theme/bootstrapbase/templates/block_timeline/event-list-item.mustache @@ -0,0 +1,63 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/event-list-item + + This template renders an event list item for the timeline block. + + Example context (json): + { + "name": "Assignment due 1", + "url": "https://www.google.com", + "timesort": 1490320388, + "course": { + "fullnamedisplay": "Course 1" + }, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "showitemcount": true, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + } +}} +
    • + +
      + {{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}} +
      + + {{#userdate}} {{timesort}}, {{#str}} strftimetime24, core_langconfig {{/str}} {{/userdate}} + +
      +
      {{{name}}}
      + {{{course.fullnamedisplay}}} +
      +
      +
    • diff --git a/theme/bootstrapbase/templates/block_timeline/event-list-items.mustache b/theme/bootstrapbase/templates/block_timeline/event-list-items.mustache new file mode 100644 index 00000000000..59da36e7dcb --- /dev/null +++ b/theme/bootstrapbase/templates/block_timeline/event-list-items.mustache @@ -0,0 +1,70 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/event-list-items + + This template renders a group of event list items for the timeline block. + + Example context (json): + { + "events": [ + { + "name": "Assignment due 1", + "url": "https://www.google.com", + "timesort": 1490320388, + "course": { + "fullnamedisplay": "Course 1" + }, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + }, + { + "name": "Assignment due 2", + "url": "https://www.google.com", + "timesort": 1490320388, + "course": { + "fullnamedisplay": "Course 1" + }, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + } + ] + } +}} +
        +{{#events}} + {{> block_timeline/event-list-item }} +{{/events}} +
      diff --git a/theme/bootstrapbase/templates/block_timeline/event-list.mustache b/theme/bootstrapbase/templates/block_timeline/event-list.mustache new file mode 100644 index 00000000000..12801c55526 --- /dev/null +++ b/theme/bootstrapbase/templates/block_timeline/event-list.mustache @@ -0,0 +1,55 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/event-list + + This template renders a list of events for the timeline block. + + Example context (json): + { + } +}} +
      +
      +
        + {{> block_timeline/placeholder-event-list-item }} + {{> block_timeline/placeholder-event-list-item }} + {{> block_timeline/placeholder-event-list-item }} + {{> block_timeline/placeholder-event-list-item }} + {{> block_timeline/placeholder-event-list-item }} +
      +
      +
      +
      +
      +
      +
      + +
      diff --git a/theme/bootstrapbase/templates/block_timeline/main.mustache b/theme/bootstrapbase/templates/block_timeline/main.mustache new file mode 100644 index 00000000000..7e82e6fe667 --- /dev/null +++ b/theme/bootstrapbase/templates/block_timeline/main.mustache @@ -0,0 +1,50 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/main + + This template renders the main content area for the timeline block. + + Example context (json): + {} +}} + +
      +
      +
      + {{> block_timeline/nav-day-filter }} + {{> block_timeline/nav-view-selector }} +
      +
      +
      + {{> block_timeline/view }} +
      +
      +{{#js}} +require( +[ + 'jquery', + 'block_timeline/main', +], +function( + $, + Main +) { + var root = $('#block-timeline-{{uniqid}}'); + Main.init(root); +}); +{{/js}} diff --git a/theme/bootstrapbase/templates/block_timeline/nav-day-filter.mustache b/theme/bootstrapbase/templates/block_timeline/nav-day-filter.mustache new file mode 100644 index 00000000000..85a1183b952 --- /dev/null +++ b/theme/bootstrapbase/templates/block_timeline/nav-day-filter.mustache @@ -0,0 +1,67 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/nav-day-filter + + This template renders the day range selector for the timeline view. + + Example context (json): + {} +}} + diff --git a/theme/bootstrapbase/templates/block_timeline/nav-view-selector.mustache b/theme/bootstrapbase/templates/block_timeline/nav-view-selector.mustache new file mode 100644 index 00000000000..4caeda98a39 --- /dev/null +++ b/theme/bootstrapbase/templates/block_timeline/nav-view-selector.mustache @@ -0,0 +1,46 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/nav-view-selector + + This template renders the timeline sort selector. + + Example context (json): + {} +}} +
      + + +
      diff --git a/theme/bootstrapbase/templates/block_timeline/placeholder-event-list-item.mustache b/theme/bootstrapbase/templates/block_timeline/placeholder-event-list-item.mustache new file mode 100644 index 00000000000..3e98db15078 --- /dev/null +++ b/theme/bootstrapbase/templates/block_timeline/placeholder-event-list-item.mustache @@ -0,0 +1,31 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/event-list-item + + This template renders an event list item loading placeholder for the timeline block. + + Example context (json): + {} +}} +
    • +
      +
      +
      +
      +
      +
    • diff --git a/theme/bootstrapbase/templates/block_timeline/view.mustache b/theme/bootstrapbase/templates/block_timeline/view.mustache new file mode 100644 index 00000000000..7d67b034e8f --- /dev/null +++ b/theme/bootstrapbase/templates/block_timeline/view.mustache @@ -0,0 +1,44 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/view + + This template renders the timeline view for the timeline block. + + Example context (json): + {} +}} +
      +
      +
      + {{> block_timeline/view-dates }} +
      +
      + {{> block_timeline/view-courses }} +
      +
      +
      \ No newline at end of file -- 2.11.4.GIT