mirror of
https://github.com/moodle/moodle.git
synced 2025-08-04 16:36:37 +02:00
Merge branch 'dashboard-split-overview-block' of https://github.com/ryanwyllie/moodle
This commit is contained in:
commit
20f9b981f9
137 changed files with 5871 additions and 2450 deletions
|
@ -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"
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
define(["jquery","core/notification","core/templates","core/custom_interaction_events","block_myoverview/calendar_events_repository"],function(a,b,c,d,e){var f=86400,g={EMPTY_MESSAGE:'[data-region="empty-message"]',ROOT:'[data-region="event-list-container"]',EVENT_LIST:'[data-region="event-list"]',EVENT_LIST_CONTENT:'[data-region="event-list-content"]',EVENT_LIST_GROUP_CONTAINER:'[data-region="event-list-group-container"]',LOADING_ICON_CONTAINER:'[data-region="loading-icon-container"]',VIEW_MORE_BUTTON:'[data-action="view-more"]'},h={EVENT_LIST_ITEMS:"block_myoverview/event-list-items",COURSE_EVENT_LIST_ITEMS:"block_myoverview/course-event-list-items"},i=function(a){a.attr("data-loaded-all",!0)},j=function(a){return!!a.attr("data-loaded-all")},k=function(a){var b=a.find(g.LOADING_ICON_CONTAINER),c=a.find(g.VIEW_MORE_BUTTON);a.addClass("loading"),b.removeClass("hidden"),c.prop("disabled",!0)},l=function(a){var b=a.find(g.LOADING_ICON_CONTAINER),c=a.find(g.VIEW_MORE_BUTTON);a.removeClass("loading"),b.addClass("hidden"),j(a)?c.addClass("hidden"):c.prop("disabled",!1)},m=function(a){return a.hasClass("loading")},n=function(a){a.attr("data-has-events",!0)},o=function(a){return!!a.attr("data-has-events")},p=function(a,b){b?n(a):o(a)||q(a)},q=function(a){a.find(g.EVENT_LIST_CONTENT).addClass("hidden"),a.find(g.EMPTY_MESSAGE).removeClass("hidden")},r=function(a,b,d){return a.removeClass("hidden"),c.render(d,{events:b}).done(function(b,d){c.appendNodeContents(a.find(g.EVENT_LIST),b,d)})},s=function(a,b){var c=b.timesort||0;return c-a},t=function(a,b,c){var d=a.attr("data-midnight"),e=+c.attr("data-start-day")*f,g=+c.attr("data-end-day")*f,h=s(d,b);return""===c.attr("data-end-day")?e<=h:e<=h&&h<g},u=function(b,c){return function(d){return t(b,d,a(c))}},v=function(b,c){var d=0,e=h.EVENT_LIST_ITEMS;return b.attr("data-course-id")&&(e=h.COURSE_EVENT_LIST_ITEMS),a.when.apply(a,a.map(b.find(g.EVENT_LIST_GROUP_CONTAINER),function(f){var g=c.filter(u(b,f));return g.length?(d+=g.length,r(a(f),g,e)):null})).then(function(){return d})},w=function(c,d){c=a(c);var g=+c.attr("data-limit"),h=+c.attr("data-course-id"),j=c.attr("data-last-id"),n=c.attr("data-midnight"),o=n-14*f;if(m(c))return a.Deferred().resolve();if(k(c),"undefined"==typeof d){var q={starttime:o,limit:g};j&&(q.aftereventid=j),h?(q.courseid=h,d=e.queryByCourse(q)):d=e.queryByTime(q)}return d.then(function(a){if(!a.events.length)return i(c),0;var b=a.events;return c.attr("data-last-id",b[b.length-1].id),b.length<g&&i(c),v(c,b).then(function(a){return a<b.length&&i(c),b.length})}).then(function(a){return p(c,a)}).fail(b.exception).always(function(){l(c)})},x=function(a){d.define(a,[d.events.activate]),a.on(d.events.activate,g.VIEW_MORE_BUTTON,function(){w(a)})};return{init:function(b){b=a(b),w(b),x(b)},registerEventListeners:x,load:w,rootSelector:g.ROOT}});
|
|
@ -1 +0,0 @@
|
|||
define(["jquery","block_myoverview/event_list","block_myoverview/calendar_events_repository"],function(a,b,c){var d=86400,e={EVENTS_BY_COURSE_CONTAINER:'[data-region="course-events-container"]',EVENT_LIST_CONTAINER:'[data-region="event-list-container"]'},f=function(f){var g=f.find(e.EVENTS_BY_COURSE_CONTAINER);if(g.length){var h=g.find(e.EVENT_LIST_CONTAINER).first(),i=h.attr("data-midnight"),j=i-14*d,k=h.attr("data-limit"),l=g.map(function(){return a(this).attr("data-course-id")}).get(),m=c.queryByCourses({courseids:l,starttime:j,limit:k});g.each(function(c,d){d=a(d);var e=d.attr("data-course-id"),f=d.find(b.rootSelector),g=a.Deferred();m.done(function(a){var b=[],c=a.groupedbycourse.filter(function(a){return a.courseid==e});c.length&&(b=c[0].events),g.resolve({events:b})}).fail(function(a){g.reject(a)}),b.load(f,g)})}};return{init:function(b){b=a(b),f(b)}}});
|
|
@ -1 +0,0 @@
|
|||
define(["jquery","core/ajax","core/custom_interaction_events","core/notification"],function(a,b,c,d){var e=function(e){c.define(e,[c.events.activate]),e.on(c.events.activate,"[data-toggle='tab']",function(c){var e=a(c.currentTarget).data("tabname");"function"==typeof window.history.pushState&&window.history.pushState(null,null,"?myoverviewtab="+e);var f={methodname:"core_user_update_user_preferences",args:{preferences:[{type:"block_myoverview_last_tab",value:e}]}};b.call([f])[0].fail(d.exception)})};return{registerEventListeners:e}});
|
|
@ -1,416 +0,0 @@
|
|||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Javascript to load and render the list of calendar events for a
|
||||
* given day range.
|
||||
*
|
||||
* @module block_myoverview/event_list
|
||||
* @package block_myoverview
|
||||
* @copyright 2016 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(['jquery', 'core/notification', 'core/templates',
|
||||
'core/custom_interaction_events',
|
||||
'block_myoverview/calendar_events_repository'],
|
||||
function($, Notification, Templates, CustomEvents, CalendarEventsRepository) {
|
||||
|
||||
var SECONDS_IN_DAY = 60 * 60 * 24;
|
||||
|
||||
var SELECTORS = {
|
||||
EMPTY_MESSAGE: '[data-region="empty-message"]',
|
||||
ROOT: '[data-region="event-list-container"]',
|
||||
EVENT_LIST: '[data-region="event-list"]',
|
||||
EVENT_LIST_CONTENT: '[data-region="event-list-content"]',
|
||||
EVENT_LIST_GROUP_CONTAINER: '[data-region="event-list-group-container"]',
|
||||
LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]',
|
||||
VIEW_MORE_BUTTON: '[data-action="view-more"]'
|
||||
};
|
||||
|
||||
var TEMPLATES = {
|
||||
EVENT_LIST_ITEMS: 'block_myoverview/event-list-items',
|
||||
COURSE_EVENT_LIST_ITEMS: 'block_myoverview/course-event-list-items'
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a flag on the element to indicate that it has completed
|
||||
* loading all event data.
|
||||
*
|
||||
* @method setLoadedAll
|
||||
* @private
|
||||
* @param {object} root The container element
|
||||
*/
|
||||
var setLoadedAll = function(root) {
|
||||
root.attr('data-loaded-all', true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if all event data has finished loading.
|
||||
*
|
||||
* @method hasLoadedAll
|
||||
* @private
|
||||
* @param {object} root The container element
|
||||
* @return {bool} if the element has completed all loading
|
||||
*/
|
||||
var hasLoadedAll = function(root) {
|
||||
return !!root.attr('data-loaded-all');
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the element state to loading.
|
||||
*
|
||||
* @method startLoading
|
||||
* @private
|
||||
* @param {object} root The container element
|
||||
*/
|
||||
var startLoading = function(root) {
|
||||
var loadingIcon = root.find(SELECTORS.LOADING_ICON_CONTAINER),
|
||||
viewMoreButton = root.find(SELECTORS.VIEW_MORE_BUTTON);
|
||||
|
||||
root.addClass('loading');
|
||||
loadingIcon.removeClass('hidden');
|
||||
viewMoreButton.prop('disabled', true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the loading state from the element.
|
||||
*
|
||||
* @method stopLoading
|
||||
* @private
|
||||
* @param {object} root The container element
|
||||
*/
|
||||
var stopLoading = function(root) {
|
||||
var loadingIcon = root.find(SELECTORS.LOADING_ICON_CONTAINER),
|
||||
viewMoreButton = root.find(SELECTORS.VIEW_MORE_BUTTON);
|
||||
|
||||
root.removeClass('loading');
|
||||
loadingIcon.addClass('hidden');
|
||||
|
||||
if (!hasLoadedAll(root)) {
|
||||
// Only enable the button if we've got more events to load.
|
||||
viewMoreButton.prop('disabled', false);
|
||||
} else {
|
||||
viewMoreButton.addClass('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the element is currently loading some event data.
|
||||
*
|
||||
* @method isLoading
|
||||
* @private
|
||||
* @param {object} root The container element
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
var isLoading = function(root) {
|
||||
return root.hasClass('loading');
|
||||
};
|
||||
|
||||
/**
|
||||
* Flag the root element to remember that it contains events.
|
||||
*
|
||||
* @method setHasContent
|
||||
* @private
|
||||
* @param {object} root The container element
|
||||
*/
|
||||
var setHasContent = function(root) {
|
||||
root.attr('data-has-events', true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the root element has had events loaded.
|
||||
*
|
||||
* @method hasContent
|
||||
* @private
|
||||
* @param {object} root The container element
|
||||
* @return {bool}
|
||||
*/
|
||||
var hasContent = function(root) {
|
||||
return root.attr('data-has-events') ? true : false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the visibility of the content area. The content area
|
||||
* is hidden if we have no events.
|
||||
*
|
||||
* @method updateContentVisibility
|
||||
* @private
|
||||
* @param {object} root The container element
|
||||
* @param {int} eventCount A count of the events we just received.
|
||||
*/
|
||||
var updateContentVisibility = function(root, eventCount) {
|
||||
if (eventCount) {
|
||||
// We've rendered some events, let's remember that.
|
||||
setHasContent(root);
|
||||
} else {
|
||||
// If this is the first time trying to load events and
|
||||
// we don't have any then there isn't any so let's show
|
||||
// the empty message.
|
||||
if (!hasContent(root)) {
|
||||
hideContent(root);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Hide the content area and display the empty content message.
|
||||
*
|
||||
* @method hideContent
|
||||
* @private
|
||||
* @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');
|
||||
};
|
||||
|
||||
/**
|
||||
* Render a group of calendar events and add them to the event
|
||||
* list.
|
||||
*
|
||||
* @method renderGroup
|
||||
* @private
|
||||
* @param {object} group The group container element
|
||||
* @param {array} calendarEvents The list of calendar events
|
||||
* @param {string} templateName The template name
|
||||
* @return {promise} Resolved when the elements are attached to the DOM
|
||||
*/
|
||||
var renderGroup = function(group, calendarEvents, templateName) {
|
||||
|
||||
group.removeClass('hidden');
|
||||
|
||||
return Templates.render(
|
||||
templateName,
|
||||
{events: calendarEvents}
|
||||
).done(function(html, js) {
|
||||
Templates.appendNodeContents(group.find(SELECTORS.EVENT_LIST), html, js);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the time (in seconds) from the given timestamp until the calendar
|
||||
* event will need actioning.
|
||||
*
|
||||
* @method timeUntilEvent
|
||||
* @private
|
||||
* @param {int} timestamp The time to compare with
|
||||
* @param {object} event The calendar event
|
||||
* @return {int}
|
||||
*/
|
||||
var timeUntilEvent = function(timestamp, event) {
|
||||
var orderTime = event.timesort || 0;
|
||||
return orderTime - timestamp;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the given calendar event should be added to the given event
|
||||
* list group container. The event list group container will specify a
|
||||
* day range for the time boundary it is interested in.
|
||||
*
|
||||
* If only a start day is specified for the container then it will be treated
|
||||
* as an open catchment for all events that begin after that time.
|
||||
*
|
||||
* @method eventBelongsInContainer
|
||||
* @private
|
||||
* @param {object} root The root element
|
||||
* @param {object} event The calendar event
|
||||
* @param {object} container The group event list container
|
||||
* @return {bool}
|
||||
*/
|
||||
var eventBelongsInContainer = function(root, event, container) {
|
||||
var todayTime = root.attr('data-midnight'),
|
||||
timeUntilContainerStart = +container.attr('data-start-day') * SECONDS_IN_DAY,
|
||||
timeUntilContainerEnd = +container.attr('data-end-day') * SECONDS_IN_DAY,
|
||||
timeUntilEventNeedsAction = timeUntilEvent(todayTime, event);
|
||||
|
||||
if (container.attr('data-end-day') === '') {
|
||||
return timeUntilContainerStart <= timeUntilEventNeedsAction;
|
||||
} else {
|
||||
return timeUntilContainerStart <= timeUntilEventNeedsAction &&
|
||||
timeUntilEventNeedsAction < timeUntilContainerEnd;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a function that can be used to filter a list of events based on the day
|
||||
* range specified on the given event list group container.
|
||||
*
|
||||
* @method getFilterCallbackForContainer
|
||||
* @private
|
||||
* @param {object} root The root element
|
||||
* @param {object} container Event list group container
|
||||
* @return {function}
|
||||
*/
|
||||
var getFilterCallbackForContainer = function(root, container) {
|
||||
return function(event) {
|
||||
return eventBelongsInContainer(root, event, $(container));
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the given calendar events in the container element. The container
|
||||
* elements must have a day range defined using data attributes that will be
|
||||
* used to group the calendar events according to their order time.
|
||||
*
|
||||
* @method render
|
||||
* @private
|
||||
* @param {object} root The container element
|
||||
* @param {array} calendarEvents A list of calendar events
|
||||
* @return {promise} Resolved with a count of the number of rendered events
|
||||
*/
|
||||
var render = function(root, calendarEvents) {
|
||||
var renderCount = 0;
|
||||
var templateName = TEMPLATES.EVENT_LIST_ITEMS;
|
||||
|
||||
if (root.attr('data-course-id')) {
|
||||
templateName = TEMPLATES.COURSE_EVENT_LIST_ITEMS;
|
||||
}
|
||||
|
||||
// Loop over each of the element list groups and find the set of calendar events
|
||||
// that belong to that group (as defined by the group's day range). The matching
|
||||
// list of calendar events are rendered and added to the DOM within that group.
|
||||
return $.when.apply($, $.map(root.find(SELECTORS.EVENT_LIST_GROUP_CONTAINER), function(container) {
|
||||
var events = calendarEvents.filter(getFilterCallbackForContainer(root, container));
|
||||
|
||||
if (events.length) {
|
||||
renderCount += events.length;
|
||||
return renderGroup($(container), events, templateName);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})).then(function() {
|
||||
return renderCount;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve a list of calendar events, render and append them to the end of the
|
||||
* existing list. The events will be loaded based on the set of data attributes
|
||||
* on the root element.
|
||||
*
|
||||
* This function can be provided with a jQuery promise. If it is then it won't
|
||||
* attempt to load data by itself, instead it will use the given promise.
|
||||
*
|
||||
* The provided promise must resolve with an an object that has an events key
|
||||
* and value is an array of calendar events.
|
||||
* E.g.
|
||||
* { events: ['event 1', 'event 2'] }
|
||||
*
|
||||
* @method load
|
||||
* @param {object} root The root element of the event list
|
||||
* @param {object} promise A jQuery promise resolved with events
|
||||
* @return {promise} A jquery promise
|
||||
*/
|
||||
var load = function(root, promise) {
|
||||
root = $(root);
|
||||
var limit = +root.attr('data-limit'),
|
||||
courseId = +root.attr('data-course-id'),
|
||||
lastId = root.attr('data-last-id'),
|
||||
midnight = root.attr('data-midnight'),
|
||||
startTime = midnight - (14 * SECONDS_IN_DAY);
|
||||
|
||||
// Don't load twice.
|
||||
if (isLoading(root)) {
|
||||
return $.Deferred().resolve();
|
||||
}
|
||||
|
||||
startLoading(root);
|
||||
|
||||
// If we haven't been provided a promise to resolve the
|
||||
// data then we will load our own.
|
||||
if (typeof promise == 'undefined') {
|
||||
var args = {
|
||||
starttime: startTime,
|
||||
limit: limit,
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
args.aftereventid = lastId;
|
||||
}
|
||||
|
||||
// If we have a course id then we only want events from that course.
|
||||
if (courseId) {
|
||||
args.courseid = courseId;
|
||||
promise = CalendarEventsRepository.queryByCourse(args);
|
||||
} else {
|
||||
// Otherwise we want events from any course.
|
||||
promise = CalendarEventsRepository.queryByTime(args);
|
||||
}
|
||||
}
|
||||
|
||||
// Request data from the server.
|
||||
return promise.then(function(result) {
|
||||
if (!result.events.length) {
|
||||
// No events, nothing to do.
|
||||
setLoadedAll(root);
|
||||
return 0;
|
||||
}
|
||||
|
||||
var calendarEvents = result.events;
|
||||
|
||||
// Remember the last id we've seen.
|
||||
root.attr('data-last-id', calendarEvents[calendarEvents.length - 1].id);
|
||||
|
||||
if (calendarEvents.length < limit) {
|
||||
// No more events to load, disable loading button.
|
||||
setLoadedAll(root);
|
||||
}
|
||||
|
||||
// Render the events.
|
||||
return render(root, calendarEvents).then(function(renderCount) {
|
||||
if (renderCount < calendarEvents.length) {
|
||||
// If the number of events that was rendered is less than
|
||||
// the number we sent for rendering we can assume that there
|
||||
// are no groups to add them in. Since the ordering of the
|
||||
// events is guaranteed it means that any future requests will
|
||||
// also yield events that can't be rendered, so let's not bother
|
||||
// sending any more requests.
|
||||
setLoadedAll(root);
|
||||
}
|
||||
return calendarEvents.length;
|
||||
});
|
||||
}).then(function(eventCount) {
|
||||
return updateContentVisibility(root, eventCount);
|
||||
}).fail(
|
||||
Notification.exception
|
||||
).always(function() {
|
||||
stopLoading(root);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Register the event listeners for the container element.
|
||||
*
|
||||
* @method registerEventListeners
|
||||
* @param {object} root The root element of the event list
|
||||
*/
|
||||
var registerEventListeners = function(root) {
|
||||
CustomEvents.define(root, [CustomEvents.events.activate]);
|
||||
root.on(CustomEvents.events.activate, SELECTORS.VIEW_MORE_BUTTON, function() {
|
||||
load(root);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
init: function(root) {
|
||||
root = $(root);
|
||||
load(root);
|
||||
registerEventListeners(root);
|
||||
},
|
||||
registerEventListeners: registerEventListeners,
|
||||
load: load,
|
||||
rootSelector: SELECTORS.ROOT,
|
||||
};
|
||||
});
|
|
@ -1,108 +0,0 @@
|
|||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Javascript to load and render the list of calendar events grouping by course.
|
||||
*
|
||||
* @module block_myoverview/events_by_course_list
|
||||
* @package block_myoverview
|
||||
* @copyright 2016 Simey Lameze <simey@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'block_myoverview/event_list',
|
||||
'block_myoverview/calendar_events_repository'
|
||||
],
|
||||
function($, EventList, EventsRepository) {
|
||||
|
||||
var SECONDS_IN_DAY = 60 * 60 * 24;
|
||||
|
||||
var SELECTORS = {
|
||||
EVENTS_BY_COURSE_CONTAINER: '[data-region="course-events-container"]',
|
||||
EVENT_LIST_CONTAINER: '[data-region="event-list-container"]',
|
||||
};
|
||||
|
||||
/**
|
||||
* Loop through course events containers and load calendar events for that course.
|
||||
*
|
||||
* @method load
|
||||
* @param {Object} root The root element of sort by course list.
|
||||
*/
|
||||
var load = function(root) {
|
||||
var courseBlocks = root.find(SELECTORS.EVENTS_BY_COURSE_CONTAINER);
|
||||
|
||||
if (!courseBlocks.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var eventList = courseBlocks.find(SELECTORS.EVENT_LIST_CONTAINER).first();
|
||||
var midnight = eventList.attr('data-midnight');
|
||||
var startTime = midnight - (14 * SECONDS_IN_DAY);
|
||||
var limit = eventList.attr('data-limit');
|
||||
var courseIds = courseBlocks.map(function() {
|
||||
return $(this).attr('data-course-id');
|
||||
}).get();
|
||||
|
||||
// Load the first set of events for each course in a single request.
|
||||
// We want to avoid sending an individual request for each course because
|
||||
// there could be lots of them.
|
||||
var coursesPromise = EventsRepository.queryByCourses({
|
||||
courseids: courseIds,
|
||||
starttime: startTime,
|
||||
limit: limit
|
||||
});
|
||||
|
||||
// Load the events into each course block.
|
||||
courseBlocks.each(function(index, container) {
|
||||
container = $(container);
|
||||
var courseId = container.attr('data-course-id');
|
||||
var eventListContainer = container.find(EventList.rootSelector);
|
||||
var promise = $.Deferred();
|
||||
|
||||
// Once all of the course events have been loaded then we need
|
||||
// to extract just the ones relevant to this course block and
|
||||
// hand them to the event list to render.
|
||||
coursesPromise.done(function(result) {
|
||||
var events = [];
|
||||
// Get this course block's events from the collection returned
|
||||
// from the server.
|
||||
var courseGroup = result.groupedbycourse.filter(function(group) {
|
||||
return group.courseid == courseId;
|
||||
});
|
||||
|
||||
if (courseGroup.length) {
|
||||
events = courseGroup[0].events;
|
||||
}
|
||||
|
||||
promise.resolve({events: events});
|
||||
}).fail(function(e) {
|
||||
promise.reject(e);
|
||||
});
|
||||
|
||||
// Provide the event list with a promise that will be resolved
|
||||
// when we have received the events from the server.
|
||||
EventList.load(eventListContainer, promise);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
init: function(root) {
|
||||
root = $(root);
|
||||
load(root);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,61 +0,0 @@
|
|||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Javascript used to save the user's tab preference.
|
||||
*
|
||||
* @package block_myoverview
|
||||
* @copyright 2017 Mark Nelson <markn@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
define(['jquery', 'core/ajax', 'core/custom_interaction_events',
|
||||
'core/notification'], function($, Ajax, CustomEvents, Notification) {
|
||||
|
||||
/**
|
||||
* Registers an event that saves the user's tab preference when switching between them.
|
||||
*
|
||||
* @param {object} root The container element
|
||||
*/
|
||||
var registerEventListeners = function(root) {
|
||||
CustomEvents.define(root, [CustomEvents.events.activate]);
|
||||
root.on(CustomEvents.events.activate, "[data-toggle='tab']", function(e) {
|
||||
var tabname = $(e.currentTarget).data('tabname');
|
||||
// Bootstrap does not change the URL when using BS tabs, so need to do this here.
|
||||
// Also check to make sure the browser supports the history API.
|
||||
if (typeof window.history.pushState === "function") {
|
||||
window.history.pushState(null, null, '?myoverviewtab=' + tabname);
|
||||
}
|
||||
var request = {
|
||||
methodname: 'core_user_update_user_preferences',
|
||||
args: {
|
||||
preferences: [
|
||||
{
|
||||
type: 'block_myoverview_last_tab',
|
||||
value: tabname
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
Ajax.call([request])[0]
|
||||
.fail(Notification.exception);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
registerEventListeners: registerEventListeners
|
||||
};
|
||||
});
|
|
@ -50,16 +50,7 @@ class block_myoverview extends block_base {
|
|||
return $this->content;
|
||||
}
|
||||
|
||||
// Check if the tab to select wasn't passed in the URL, if so see if the user has any preference.
|
||||
if (!$tab = optional_param('myoverviewtab', null, PARAM_ALPHA)) {
|
||||
// Check if the user has no preference, if so get the site setting.
|
||||
if (!$tab = get_user_preferences('block_myoverview_last_tab')) {
|
||||
$config = get_config('block_myoverview');
|
||||
$tab = $config->defaulttab;
|
||||
}
|
||||
}
|
||||
|
||||
$renderable = new \block_myoverview\output\main($tab);
|
||||
$renderable = new \block_myoverview\output\main();
|
||||
$renderer = $this->page->get_renderer('block_myoverview');
|
||||
|
||||
$this->content = new stdClass();
|
||||
|
@ -77,13 +68,4 @@ class block_myoverview extends block_base {
|
|||
public function applicable_formats() {
|
||||
return array('my' => true);
|
||||
}
|
||||
|
||||
/**
|
||||
* This block does contain a configuration settings.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function has_config() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ use renderer_base;
|
|||
use templatable;
|
||||
use core_completion\progress;
|
||||
|
||||
require_once($CFG->dirroot . '/blocks/myoverview/lib.php');
|
||||
require_once($CFG->libdir . '/completionlib.php');
|
||||
|
||||
/**
|
||||
|
@ -39,21 +38,6 @@ require_once($CFG->libdir . '/completionlib.php');
|
|||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class main implements renderable, templatable {
|
||||
|
||||
/**
|
||||
* @var string The tab to display.
|
||||
*/
|
||||
public $tab;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $tab The tab to display.
|
||||
*/
|
||||
public function __construct($tab) {
|
||||
$this->tab = $tab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export this data so it can be used as the context for a mustache template.
|
||||
*
|
||||
|
@ -86,26 +70,13 @@ class main implements renderable, templatable {
|
|||
|
||||
$coursesview = new courses_view($courses, $coursesprogress);
|
||||
$nocoursesurl = $output->image_url('courses', 'block_myoverview')->out();
|
||||
$noeventsurl = $output->image_url('activities', 'block_myoverview')->out();
|
||||
|
||||
// Now, set the tab we are going to be viewing.
|
||||
$viewingtimeline = false;
|
||||
$viewingcourses = false;
|
||||
if ($this->tab == BLOCK_MYOVERVIEW_TIMELINE_VIEW) {
|
||||
$viewingtimeline = true;
|
||||
} else {
|
||||
$viewingcourses = true;
|
||||
}
|
||||
|
||||
return [
|
||||
'midnight' => usergetmidnight(time()),
|
||||
'coursesview' => $coursesview->export_for_template($output),
|
||||
'urls' => [
|
||||
'nocourses' => $nocoursesurl,
|
||||
'noevents' => $noeventsurl
|
||||
],
|
||||
'viewingtimeline' => $viewingtimeline,
|
||||
'viewingcourses' => $viewingcourses
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,30 +32,15 @@ defined('MOODLE_INTERNAL') || die();
|
|||
* @copyright 2018 Zig Tan <zig@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\user_preference_provider {
|
||||
class provider implements \core_privacy\local\metadata\null_provider {
|
||||
|
||||
/**
|
||||
* Returns meta-data information about the myoverview block.
|
||||
* Get the language string identifier with the component's language
|
||||
* file to explain why this plugin stores no data.
|
||||
*
|
||||
* @param \core_privacy\local\metadata\collection $collection A collection of meta-data.
|
||||
* @return \core_privacy\local\metadata\collection Return the collection of meta-data.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_metadata(\core_privacy\local\metadata\collection $collection) :
|
||||
\core_privacy\local\metadata\collection {
|
||||
$collection->add_user_preference('block_myoverview_last_tab', 'privacy:metadata:overviewlasttab');
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user preferences for the myoverview block
|
||||
*
|
||||
* @param int $userid The userid of the user whose data is to be exported.
|
||||
*/
|
||||
public static function export_user_preferences(int $userid) {
|
||||
$preference = get_user_preferences('block_myoverview_last_tab', null, $userid);
|
||||
if (isset($preference)) {
|
||||
\core_privacy\local\request\writer::export_user_preference('block_myoverview', 'block_myoverview_last_tab',
|
||||
$preference, get_string('privacy:metadata:overviewlasttab', 'block_myoverview'));
|
||||
}
|
||||
public static function get_reason() : string {
|
||||
return 'privacy:metadata';
|
||||
}
|
||||
}
|
||||
|
|
41
blocks/myoverview/db/upgrade.php
Normal file
41
blocks/myoverview/db/upgrade.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* This file keeps track of upgrades to the myoverview block
|
||||
*
|
||||
* @package block_myoverview
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Upgrade code for the myoverview block.
|
||||
*
|
||||
* @param int $oldversion
|
||||
*/
|
||||
function xmldb_block_myoverview_upgrade($oldversion) {
|
||||
global $DB;
|
||||
|
||||
if ($oldversion < 2018092700) {
|
||||
$DB->delete_records('user_preferences', ['name' => 'block_myoverview_last_tab']);
|
||||
upgrade_block_savepoint(true, 2018092700, 'myoverview');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
|
@ -22,8 +22,6 @@
|
|||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['defaulttab'] = 'Default tab';
|
||||
$string['defaulttab_desc'] = 'The tab that will be displayed when a user first views their course overview. When returning to their course overview, the user\'s active tab is remembered.';
|
||||
$string['future'] = 'Future';
|
||||
$string['inprogress'] = 'In progress';
|
||||
$string['morecourses'] = 'More courses';
|
||||
|
@ -33,15 +31,8 @@ $string['nocourses'] = 'No courses';
|
|||
$string['nocoursesinprogress'] = 'No in progress courses';
|
||||
$string['nocoursesfuture'] = 'No future courses';
|
||||
$string['nocoursespast'] = 'No past courses';
|
||||
$string['noevents'] = 'No upcoming activities due';
|
||||
$string['next30days'] = 'Next 30 days';
|
||||
$string['next7days'] = 'Next 7 days';
|
||||
$string['past'] = 'Past';
|
||||
$string['pluginname'] = 'Course overview';
|
||||
$string['recentlyoverdue'] = 'Recently overdue';
|
||||
$string['sortbycourses'] = 'Sort by courses';
|
||||
$string['sortbydates'] = 'Sort by dates';
|
||||
$string['timeline'] = 'Timeline';
|
||||
$string['viewcourse'] = 'View course';
|
||||
$string['viewcoursename'] = 'View course {$a}';
|
||||
$string['privacy:metadata:overviewlasttab'] = 'This stores the last tab selected by the user on the overview block.';
|
||||
$string['privacy:metadata'] = 'The myoverview block does not store any personal data.';
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_myoverview/course-event-list-item
|
||||
|
||||
This template renders an event list item for the myoverview block
|
||||
in the courses view.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"name": "Assignment due 1",
|
||||
"url": "https://www.google.com",
|
||||
"timesort": 1490320388,
|
||||
"action": {
|
||||
"name": "Submit assignment",
|
||||
"url": "https://www.google.com",
|
||||
"itemcount": 1,
|
||||
"showitemcount": true,
|
||||
"actionable": true
|
||||
},
|
||||
"icon": {
|
||||
"key": "icon",
|
||||
"component": "mod_assign",
|
||||
"alttext": "Assignment icon"
|
||||
}
|
||||
}
|
||||
}}
|
||||
<li class="list-group-item event-list-item" data-region="event-list-item">
|
||||
<div class="row">
|
||||
<div class="col-lg-7 col-xl-8">
|
||||
<div class="d-inline-block icon-large event-icon">
|
||||
{{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}}
|
||||
</div>
|
||||
<div class="d-inline-block event-name-container">
|
||||
<a class="event-name text-truncate" href="{{{url}}}">{{{name}}}</a>
|
||||
<p class="small text-muted text-truncate m-b-0">
|
||||
{{#userdate}} {{timesort}}, {{#str}} strftimerecent {{/str}} {{/userdate}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden-md-down d-none d-md-block col-lg-5 col-xl-4 text-truncate">
|
||||
{{#action.actionable}}
|
||||
<a href="{{{action.url}}}">{{action.name}}</a>
|
||||
{{#action.itemcount}}
|
||||
{{#action.showitemcount}}
|
||||
<span class="tag tag-pill tag-default">{{.}}</span>
|
||||
{{/action.showitemcount}}
|
||||
{{/action.itemcount}}
|
||||
{{/action.actionable}}
|
||||
{{^action.actionable}}
|
||||
<div class="text-muted">{{action.name}}</div>
|
||||
{{/action.actionable}}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
|
@ -1,110 +0,0 @@
|
|||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_myoverview/course-event-list
|
||||
|
||||
This template renders a list of events for the myoverview block
|
||||
sort by courses view.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"urls": {
|
||||
"noevents": "#"
|
||||
}
|
||||
}
|
||||
}}
|
||||
<div data-region="event-list-container"
|
||||
data-limit="{{$limit}}20{{/limit}}"
|
||||
data-course-id="{{$courseid}}{{/courseid}}"
|
||||
data-last-id="{{$lastid}}{{/lastid}}"
|
||||
data-midnight="{{midnight}}"
|
||||
id="event-list-container-{{$courseid}}{{/courseid}}">
|
||||
|
||||
<div data-region="event-list-content">
|
||||
{{< block_myoverview/event-list-group }}
|
||||
{{$title}}{{#str}} recentlyoverdue, block_myoverview {{/str}}{{/title}}
|
||||
{{$extratitleclasses}}text-danger{{/extratitleclasses}}
|
||||
{{$startday}}-14{{/startday}}
|
||||
{{$endday}}0{{/endday}}
|
||||
{{$eventlistitems}}
|
||||
{{> block_myoverview/course-event-list-items }}
|
||||
{{/eventlistitems}}
|
||||
{{/ block_myoverview/event-list-group }}
|
||||
{{< block_myoverview/event-list-group }}
|
||||
{{$title}}{{#str}} today {{/str}}{{/title}}
|
||||
{{$extratitleclasses}}{{/extratitleclasses}}
|
||||
{{$startday}}0{{/startday}}
|
||||
{{$endday}}1{{/endday}}
|
||||
{{$eventlistitems}}
|
||||
{{> block_myoverview/course-event-list-items }}
|
||||
{{/eventlistitems}}
|
||||
{{/ block_myoverview/event-list-group }}
|
||||
{{< block_myoverview/event-list-group }}
|
||||
{{$title}}{{#str}} next7days, block_myoverview {{/str}}{{/title}}
|
||||
{{$extratitleclasses}}{{/extratitleclasses}}
|
||||
{{$startday}}1{{/startday}}
|
||||
{{$endday}}7{{/endday}}
|
||||
{{$eventlistitems}}
|
||||
{{> block_myoverview/course-event-list-items }}
|
||||
{{/eventlistitems}}
|
||||
{{/ block_myoverview/event-list-group }}
|
||||
{{< block_myoverview/event-list-group }}
|
||||
{{$title}}{{#str}} next30days, block_myoverview {{/str}}{{/title}}
|
||||
{{$extratitleclasses}}{{/extratitleclasses}}
|
||||
{{$startday}}7{{/startday}}
|
||||
{{$endday}}30{{/endday}}
|
||||
{{$eventlistitems}}
|
||||
{{> block_myoverview/course-event-list-items }}
|
||||
{{/eventlistitems}}
|
||||
{{/ block_myoverview/event-list-group }}
|
||||
{{< block_myoverview/event-list-group }}
|
||||
{{$title}}{{#str}} future, block_myoverview {{/str}}{{/title}}
|
||||
{{$extratitleclasses}}{{/extratitleclasses}}
|
||||
{{$startday}}30{{/startday}}
|
||||
{{$endday}}{{/endday}}
|
||||
{{$eventlistitems}}
|
||||
{{> block_myoverview/course-event-list-items }}
|
||||
{{/eventlistitems}}
|
||||
{{/ block_myoverview/event-list-group }}
|
||||
|
||||
<div class="text-xs-center text-center m-y-2">
|
||||
<button type="button" class="btn btn-secondary" data-action="view-more">
|
||||
{{#str}} viewmore {{/str}}
|
||||
<span class="hidden" data-region="loading-icon-container">
|
||||
{{> core/loading }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden text-xs-center text-center m-y-3" data-region="empty-message">
|
||||
<img class="empty-placeholder-image-sm"
|
||||
src="{{urls.noevents}}"
|
||||
alt="{{#str}} noevents, block_myoverview {{/str}}"
|
||||
role="presentation">
|
||||
<p class="text-muted m-t-1">{{#str}} noevents, block_myoverview {{/str}}</p>
|
||||
<a href="{{viewurl}}" class="btn btn-secondary {{#visible}}text-primary{{/visible}}"
|
||||
aria-label="{{#str}} viewcoursename, block_myoverview, {{{fullnamedisplay}}} {{/str}}">
|
||||
{{#str}} viewcourse, block_myoverview {{/str}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{#js}}
|
||||
require(['jquery', 'block_myoverview/event_list'], function($, EventList) {
|
||||
var root = $("#event-list-container-{{$courseid}}{{/courseid}}");
|
||||
EventList.registerEventListeners(root);
|
||||
});
|
||||
{{/js}}
|
|
@ -1,49 +0,0 @@
|
|||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_myoverview/course-summary
|
||||
|
||||
This template renders the course summary (view by courses) for the myoverview block.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"fullnamedisplay": "course 3",
|
||||
"viewurl": "https://www.google.com",
|
||||
"summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
|
||||
}
|
||||
}}
|
||||
<div class="course-info-container" id="course-info-container-{{id}}">
|
||||
<div class="d-sm-none d-lg-block">
|
||||
{{> block_myoverview/progress-chart}}
|
||||
<h4 class="h5"><a href="{{viewurl}}" class="{{^visible}}dimmed{{/visible}}">{{{fullnamedisplay}}}</a></h4>
|
||||
</div>
|
||||
<div class="d-none d-sm-block d-lg-none visible-tablet">
|
||||
<div class="media">
|
||||
<div class="media-left pr-3">
|
||||
<div class="media-object">
|
||||
{{> block_myoverview/progress-chart}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<h4 class="h5"><a href="{{viewurl}}" class="{{^visible}}dimmed{{/visible}}">{{{fullnamedisplay}}}</a></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted">
|
||||
{{#shortentext}} 140, {{{summary}}}{{/shortentext}}
|
||||
</p>
|
||||
</div>
|
|
@ -0,0 +1,40 @@
|
|||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_myoverview/courses-view-nav-grouping-display-filter
|
||||
|
||||
This template renders the main content area for the myoverview block.
|
||||
|
||||
Example context (json):
|
||||
{}
|
||||
}}
|
||||
<div data-region="courses-grouping-display-filter" class="btn-group">
|
||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{#str}} inprogress, block_myoverview {{/str}}
|
||||
</button>
|
||||
<div class="dropdown-menu list-group hidden" data-show-active-item data-skip-active-class="true">
|
||||
<a class="dropdown-item active" href="#myoverview_courses_view_in_progress" data-toggle="tab">
|
||||
{{#str}} inprogress, block_myoverview {{/str}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#myoverview_courses_view_future" data-toggle="tab">
|
||||
{{#str}} future, block_myoverview {{/str}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#myoverview_courses_view_past" data-toggle="tab">
|
||||
{{#str}} past, block_myoverview {{/str}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
|
@ -24,25 +24,6 @@
|
|||
}}
|
||||
<div id="courses-view-{{uniqid}}" data-region="courses-view">
|
||||
{{#hascourses}}
|
||||
<div class="d-flex justify-content-center">
|
||||
<ul class="nav nav-pills my-5">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#myoverview_courses_view_in_progress" data-toggle="tab">
|
||||
{{#str}} inprogress, block_myoverview {{/str}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#myoverview_courses_view_future" data-toggle="tab">
|
||||
{{#str}} future, block_myoverview {{/str}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#myoverview_courses_view_past" data-toggle="tab">
|
||||
{{#str}} past, block_myoverview {{/str}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active fade show" id="myoverview_courses_view_in_progress">
|
||||
{{#inprogress}}
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_myoverview/event-list-group
|
||||
|
||||
This template renders a list of events for the myoverview block.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"enddate": "Nov 4th, 10am",
|
||||
"name": "Assignment due 1",
|
||||
"url": "https://www.google.com",
|
||||
"course": {
|
||||
"fullname": "Course 1"
|
||||
},
|
||||
"action": {
|
||||
"name": "Submit assignment",
|
||||
"url": "https://www.google.com",
|
||||
"itemcount": 1
|
||||
},
|
||||
"icon": {
|
||||
"key": "icon",
|
||||
"component": "mod_assign",
|
||||
"alttext": "Assignment icon"
|
||||
}
|
||||
},
|
||||
{
|
||||
"enddate": "Nov 4th, 10am",
|
||||
"name": "Assignment due 2",
|
||||
"url": "https://www.google.com",
|
||||
"course": {
|
||||
"fullname": "Course 1"
|
||||
},
|
||||
"action": {
|
||||
"name": "Submit assignment",
|
||||
"url": "https://www.google.com",
|
||||
"itemcount": 1
|
||||
},
|
||||
"icon": {
|
||||
"key": "icon",
|
||||
"component": "mod_assign",
|
||||
"alttext": "Assignment icon"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
<div data-region="event-list-group-container"
|
||||
data-start-day="{{$startday}}0{{/startday}}"
|
||||
data-end-day="{{$endday}}{{/endday}}"
|
||||
class="hidden">
|
||||
|
||||
<h5 class="h6 m-t-1 {{$extratitleclasses}}{{/extratitleclasses}}" id="event-list-title-{{uniqid}}"><strong>{{$title}}{{/title}}</strong></h5>
|
||||
<ul class="list-group unstyled" data-region="event-list" aria-describedby="event-list-title-{{uniqid}}">
|
||||
{{$eventlistitems}}
|
||||
{{> block_myoverview/event-list-items }}
|
||||
{{/eventlistitems}}
|
||||
</ul>
|
||||
</div>
|
|
@ -1,76 +0,0 @@
|
|||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_myoverview/event-list-item
|
||||
|
||||
This template renders an event list item for the myoverview block.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"name": "Assignment due 1",
|
||||
"url": "https://www.google.com",
|
||||
"timesort": 1490320388,
|
||||
"course": {
|
||||
"fullnamedisplay": "Course 1"
|
||||
},
|
||||
"action": {
|
||||
"name": "Submit assignment",
|
||||
"url": "https://www.google.com",
|
||||
"itemcount": 1,
|
||||
"showitemcount": true,
|
||||
"actionable": true
|
||||
},
|
||||
"icon": {
|
||||
"key": "icon",
|
||||
"component": "mod_assign",
|
||||
"alttext": "Assignment icon"
|
||||
}
|
||||
}
|
||||
}}
|
||||
<li class="list-group-item event-list-item" data-region="event-list-item">
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-lg-6 col-xl-7">
|
||||
<div class="d-inline-block icon-large event-icon">
|
||||
{{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}}
|
||||
</div>
|
||||
<div class="d-inline-block event-name-container">
|
||||
<a class="event-name text-truncate" href="{{{url}}}">{{{name}}}</a>
|
||||
<p class="small text-muted text-truncate m-b-0">{{{course.fullnamedisplay}}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4 col-lg-6 col-xl-5">
|
||||
<div class="row">
|
||||
<div class="col-lg-5 text-xs-right text-lg-left text-truncate">
|
||||
{{#userdate}} {{timesort}}, {{#str}} strftimerecent {{/str}} {{/userdate}}
|
||||
</div>
|
||||
<div class="hidden-md-down d-none d-md-block col-lg-7 text-truncate">
|
||||
{{#action.actionable}}
|
||||
<a href="{{{action.url}}}">{{action.name}}</a>
|
||||
{{#action.itemcount}}
|
||||
{{#action.showitemcount}}
|
||||
<span class="tag tag-pill tag-default">{{.}}</span>
|
||||
{{/action.showitemcount}}
|
||||
{{/action.itemcount}}
|
||||
{{/action.actionable}}
|
||||
{{^action.actionable}}
|
||||
<div class="text-muted">{{action.name}}</div>
|
||||
{{/action.actionable}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
|
@ -1,87 +0,0 @@
|
|||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_myoverview/event-list
|
||||
|
||||
This template renders a list of events for the myoverview block.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
}
|
||||
}}
|
||||
<div data-region="event-list-container"
|
||||
data-limit="{{$limit}}20{{/limit}}"
|
||||
data-course-id="{{$courseid}}{{/courseid}}"
|
||||
data-last-id="{{$lastid}}{{/lastid}}"
|
||||
data-midnight="{{midnight}}"
|
||||
id="event-list-container-{{$courseid}}{{/courseid}}">
|
||||
|
||||
<div data-region="event-list-content">
|
||||
{{< block_myoverview/event-list-group }}
|
||||
{{$title}}{{#str}} recentlyoverdue, block_myoverview {{/str}}{{/title}}
|
||||
{{$extratitleclasses}}text-danger{{/extratitleclasses}}
|
||||
{{$startday}}-14{{/startday}}
|
||||
{{$endday}}0{{/endday}}
|
||||
{{/ block_myoverview/event-list-group }}
|
||||
{{< block_myoverview/event-list-group }}
|
||||
{{$title}}{{#str}} today {{/str}}{{/title}}
|
||||
{{$extratitleclasses}}{{/extratitleclasses}}
|
||||
{{$startday}}0{{/startday}}
|
||||
{{$endday}}1{{/endday}}
|
||||
{{/ block_myoverview/event-list-group }}
|
||||
{{< block_myoverview/event-list-group }}
|
||||
{{$title}}{{#str}} next7days, block_myoverview {{/str}}{{/title}}
|
||||
{{$extratitleclasses}}{{/extratitleclasses}}
|
||||
{{$startday}}1{{/startday}}
|
||||
{{$endday}}7{{/endday}}
|
||||
{{/ block_myoverview/event-list-group }}
|
||||
{{< block_myoverview/event-list-group }}
|
||||
{{$title}}{{#str}} next30days, block_myoverview {{/str}}{{/title}}
|
||||
{{$extratitleclasses}}{{/extratitleclasses}}
|
||||
{{$startday}}7{{/startday}}
|
||||
{{$endday}}30{{/endday}}
|
||||
{{/ block_myoverview/event-list-group }}
|
||||
{{< block_myoverview/event-list-group }}
|
||||
{{$title}}{{#str}} future, block_myoverview {{/str}}{{/title}}
|
||||
{{$extratitleclasses}}{{/extratitleclasses}}
|
||||
{{$startday}}30{{/startday}}
|
||||
{{$endday}}{{/endday}}
|
||||
{{/ block_myoverview/event-list-group }}
|
||||
|
||||
<div class="text-xs-center text-center m-y-2">
|
||||
<button type="button" class="btn btn-secondary" data-action="view-more">
|
||||
{{#str}} viewmore {{/str}}
|
||||
<span class="hidden" data-region="loading-icon-container">
|
||||
{{> core/loading }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden text-xs-center text-center m-t-3" data-region="empty-message">
|
||||
<img class="empty-placeholder-image-lg"
|
||||
src="{{urls.noevents}}"
|
||||
alt="{{#str}} noevents, block_myoverview {{/str}}"
|
||||
role="presentation">
|
||||
<p class="text-muted m-t-1">{{#str}} noevents, block_myoverview {{/str}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{#js}}
|
||||
require(['jquery', 'block_myoverview/event_list'], function($, EventList) {
|
||||
var root = $("#event-list-container-{{$courseid}}{{/courseid}}");
|
||||
EventList.registerEventListeners(root);
|
||||
});
|
||||
{{/js}}
|
|
@ -24,32 +24,20 @@
|
|||
}}
|
||||
|
||||
<div id="block-myoverview-{{uniqid}}" class="block-myoverview" data-region="myoverview">
|
||||
<ul id="block-myoverview-view-choices-{{uniqid}}" class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#viewingtimeline}}active{{/viewingtimeline}}" href="#myoverview_timeline_view" role="tab" data-toggle="tab" data-tabname="timeline">
|
||||
{{#str}} timeline, block_myoverview {{/str}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#viewingcourses}}active{{/viewingcourses}}" href="#myoverview_courses_view" role="tab" data-toggle="tab" data-tabname="courses">
|
||||
{{#str}} courses {{/str}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content content-centred">
|
||||
<div role="tabpanel" class="tab-pane fade {{#viewingtimeline}}in active{{/viewingtimeline}}" id="myoverview_timeline_view">
|
||||
{{> block_myoverview/timeline-view }}
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane fade {{#viewingcourses}}in active{{/viewingcourses}}" id="myoverview_courses_view">
|
||||
<div class="container p-0 m-b-1">
|
||||
<div class="row no-gutters">
|
||||
{{#coursesview}}
|
||||
{{> block_myoverview/courses-view }}
|
||||
{{#hascourses}}
|
||||
<div class="{{#viewingtimeline}}d-none{{/viewingtimeline}}" data-tab-content="courses">
|
||||
{{> block_myoverview/courses-view-nav-grouping-display-filter }}
|
||||
</div>
|
||||
{{/hascourses}}
|
||||
{{/coursesview}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="container p-0">
|
||||
{{#coursesview}}
|
||||
{{> block_myoverview/courses-view }}
|
||||
{{/coursesview}}
|
||||
</div>
|
||||
</div>
|
||||
{{#js}}
|
||||
require(['jquery', 'block_myoverview/tab_preferences'], function($, TabPreferences) {
|
||||
var root = $('#block-myoverview-view-choices-{{uniqid}}');
|
||||
TabPreferences.registerEventListeners(root);
|
||||
});
|
||||
{{/js}}
|
||||
|
|
|
@ -1,121 +0,0 @@
|
|||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_myoverview/timeline-view-courses
|
||||
|
||||
This template renders the timeline view by courses for the myoverview block.
|
||||
|
||||
Example context (json):
|
||||
{}
|
||||
}}
|
||||
<div id="sort-by-courses-view-{{uniqid}}">
|
||||
{{#coursesview}}
|
||||
{{#inprogress}}
|
||||
{{#haspages}}
|
||||
{{#pages}}
|
||||
<ul class="list-group unstyled hidden" data-region="course-block">
|
||||
{{#courses}} {{> block_myoverview/course-item }} {{/courses}}
|
||||
</ul>
|
||||
{{/pages}}
|
||||
<div class="text-xs-center text-center m-t-1">
|
||||
<button type="button" class="btn btn-secondary" data-action="more-courses">
|
||||
{{#str}} morecourses, block_myoverview {{/str}}
|
||||
<span class="hidden" data-region="loading-icon-container">
|
||||
{{> core/loading }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{{/haspages}}
|
||||
{{^haspages}}
|
||||
<div class="text-xs-center text-center m-t-3">
|
||||
<img class="empty-placeholder-image-lg"
|
||||
src="{{urls.noevents}}"
|
||||
alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}"
|
||||
role="presentation">
|
||||
<p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p>
|
||||
</div>
|
||||
{{/haspages}}
|
||||
{{/inprogress}}
|
||||
{{^inprogress}}
|
||||
<div class="text-xs-center text-center m-t-3">
|
||||
<img class="empty-placeholder-image-lg"
|
||||
src="{{urls.noevents}}"
|
||||
alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}"
|
||||
role="presentation">
|
||||
<p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p>
|
||||
</div>
|
||||
{{/inprogress}}
|
||||
{{/coursesview}}
|
||||
</div>
|
||||
{{#js}}
|
||||
require(['jquery', 'core/custom_interaction_events', 'block_myoverview/event_list_by_course'],
|
||||
function($, CustomEvents, EventListByCourse) {
|
||||
|
||||
var root = $("#sort-by-courses-view-{{uniqid}}");
|
||||
// This flag is used so that we can delay the loading of the events until the tab
|
||||
// is toggled by the user.
|
||||
var seen = false;
|
||||
|
||||
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, '[data-action="more-courses"]', function(e, data) {
|
||||
var button = $(e.target);
|
||||
var blocks = root.find('[data-region="course-block"].hidden');
|
||||
|
||||
if (blocks && blocks.length) {
|
||||
var block = blocks.first();
|
||||
EventListByCourse.init(block);
|
||||
block.removeClass('hidden');
|
||||
}
|
||||
|
||||
// If there was only one hidden block then we have no more to show now
|
||||
// so we can disable the button.
|
||||
if (blocks && blocks.length == 1) {
|
||||
button.addClass('hidden');
|
||||
}
|
||||
|
||||
if (data) {
|
||||
data.originalEvent.preventDefault();
|
||||
data.originalEvent.stopPropagation();
|
||||
}
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// 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.closest('[data-region="timeline-view"]').on('shown shown.bs.tab', function(e) {
|
||||
if (seen) {
|
||||
return;
|
||||
}
|
||||
|
||||
var tab = $(e.target);
|
||||
var tabTarget = $(tab.attr('href'));
|
||||
|
||||
if (!tabTarget || !tabTarget.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var viewCourses = tabTarget.find('#sort-by-courses-view-{{uniqid}}');
|
||||
|
||||
if (viewCourses && viewCourses.length && !seen) {
|
||||
seen = true;
|
||||
viewCourses.find('[data-action="more-courses"]').trigger(CustomEvents.events.activate);
|
||||
}
|
||||
});
|
||||
});
|
||||
{{/js}}
|
|
@ -1,49 +0,0 @@
|
|||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_myoverview/timeline-view
|
||||
|
||||
This template renders the timeline view for the myoverview block.
|
||||
|
||||
Example context (json):
|
||||
{}
|
||||
}}
|
||||
<div id="timeline-view-{{uniqid}}" data-region="timeline-view">
|
||||
<div class="d-flex justify-content-center">
|
||||
<ul class="nav nav-pills my-5">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#myoverview_timeline_dates" data-toggle="tab">
|
||||
{{#str}} sortbydates, block_myoverview {{/str}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#myoverview_timeline_courses" data-toggle="tab">
|
||||
{{#str}} sortbycourses, block_myoverview {{/str}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active fade show" id="myoverview_timeline_dates">
|
||||
{{> block_myoverview/timeline-view-dates }}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="myoverview_timeline_courses">
|
||||
{{> block_myoverview/timeline-view-courses }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,5 +1,5 @@
|
|||
@block @block_myoverview @javascript
|
||||
Feature: The my overview block allows users to easily access their courses and see upcoming activities
|
||||
Feature: The my overview block allows users to easily access their courses
|
||||
In order to enable the my overview block in a course
|
||||
As a student
|
||||
I can add the my overview block to my dashboard
|
||||
|
@ -14,59 +14,23 @@ Feature: The my overview block allows users to easily access their courses and s
|
|||
| 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 | C3 | feedback3 | Test feedback 3 | Test feedback description | ##first day of +5 months## | ##last day of +5 months## |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
| student1 | C2 | student |
|
||||
| student1 | C3 | student |
|
||||
|
||||
Scenario: View courses and upcoming activities on timeline view
|
||||
Given I log in as "student1"
|
||||
And I click on "Timeline" "link" in the "Course overview" "block"
|
||||
When I click on "Sort by dates" "link" in the "Course overview" "block"
|
||||
Then I should see "Next 7 days" in the "Course overview" "block"
|
||||
And I should see "Test choice 1 closes" in the "Course overview" "block"
|
||||
And I should see "View choices" in the "Course overview" "block"
|
||||
And I should see "Test feedback 1 closes" in the "Course overview" "block"
|
||||
And I should see "Answer the questions" in the "Course overview" "block"
|
||||
And I should see "Future" in the "Course overview" "block"
|
||||
And I should see "Test choice 3 closes" in the "Course overview" "block"
|
||||
And I should see "Test feedback 3 closes" in the "Course overview" "block"
|
||||
And I log out
|
||||
|
||||
Scenario: Past activities should not be displayed on the timeline view
|
||||
Given I log in as "student1"
|
||||
And I click on "Timeline" "link" in the "Course overview" "block"
|
||||
When I click on "Sort by dates" "link" in the "Course overview" "block"
|
||||
And I should not see "Test choice 2 closes" in the "Course overview" "block"
|
||||
And I log out
|
||||
|
||||
Scenario: See the courses I am enrolled by their status on courses view
|
||||
Given I log in as "student1"
|
||||
And I click on "Courses" "link" in the "Course overview" "block"
|
||||
And I click on "In progress" "link" in the "Course overview" "block"
|
||||
And I should see "Course 2" in the "Course overview" "block"
|
||||
And I should not see "Course 1" in the "Course overview" "block"
|
||||
And I click on "In progress" "button" in the "Course overview" "block"
|
||||
And I click on "Future" "link" in the "Course overview" "block"
|
||||
And I should see "Course 3" in the "Course overview" "block"
|
||||
And I should not see "Course 1" in the "Course overview" "block"
|
||||
And I click on "Future" "button" in the "Course overview" "block"
|
||||
When I click on "Past" "link" in the "Course overview" "block"
|
||||
Then I should see "Course 1" in the "Course overview" "block"
|
||||
And I should not see "Course 2" in the "Course overview" "block"
|
||||
And I should not see "Course 3" in the "Course overview" "block"
|
||||
And I log out
|
||||
|
||||
Scenario: No activities should be displayed if the user is not enrolled
|
||||
Given I log in as "student2"
|
||||
And I click on "Timeline" "link" in the "Course overview" "block"
|
||||
And I should see "No upcoming activities" in the "Course overview" "block"
|
||||
When I click on "Courses" "link" in the "Course overview" "block"
|
||||
Then I should see "No courses" in the "Course overview" "block"
|
||||
And I log out
|
||||
|
|
|
@ -20,18 +20,6 @@ Feature: Course overview block show users their progress on courses
|
|||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
|
||||
Scenario: Course progress percentage should not be displayed if completion is not enabled
|
||||
Given I log in as "student1"
|
||||
And I click on "Timeline" "link" in the "Course overview" "block"
|
||||
When I click on "Sort by courses" "link" in the "Course overview" "block"
|
||||
Then I should see "Test choice 1 closes" in the "#myoverview_timeline_courses" "css_element"
|
||||
And I should not see "0%" in the "Course overview" "block"
|
||||
And I click on "Courses" "link" in the "Course overview" "block"
|
||||
And I click on "In progress" "link" in the "Course overview" "block"
|
||||
And I should see "Course 1" in the "Course overview" "block"
|
||||
And I should not see "0%" in the "Course overview" "block"
|
||||
And I log out
|
||||
|
||||
Scenario: User complete activity and verify his progress
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
|
@ -43,21 +31,11 @@ Feature: Course overview block show users their progress on courses
|
|||
And I press "Save and return to course"
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
And I click on "Sort by courses" "link" in the "Course overview" "block"
|
||||
And I should see "Test choice 1 closes" in the "#myoverview_timeline_courses" "css_element"
|
||||
And I should see "0%" in the "Course overview" "block"
|
||||
And I click on "Courses" "link" in the "Course overview" "block"
|
||||
When I click on "In progress" "link" in the "Course overview" "block"
|
||||
Then I should see "Course 1" in the "Course overview" "block"
|
||||
And I should see "0%" in the "Course overview" "block"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Test choice 1"
|
||||
And I follow "Dashboard" in the user menu
|
||||
And I click on "Timeline" "link" in the "Course overview" "block"
|
||||
And I click on "Sort by courses" "link" in the "Course overview" "block"
|
||||
And I should see "100%" in the "Course overview" "block"
|
||||
And I click on "Courses" "link" in the "Course overview" "block"
|
||||
And I click on "In progress" "link" in the "Course overview" "block"
|
||||
And I should see "Course 1" in the "Course overview" "block"
|
||||
And I should see "100%" in the "Course overview" "block"
|
||||
And I log out
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit tests for the block_myoverview implementation of the privacy API.
|
||||
*
|
||||
* @package block_myoverview
|
||||
* @category test
|
||||
* @copyright 2018 Adrian Greeve <adriangreeve.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use \core_privacy\local\request\writer;
|
||||
use \block_myoverview\privacy\provider;
|
||||
|
||||
/**
|
||||
* Unit tests for the block_myoverview implementation of the privacy API.
|
||||
*
|
||||
* @copyright 2018 Adrian Greeve <adriangreeve.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class block_myoverview_privacy_testcase extends \core_privacy\tests\provider_testcase {
|
||||
|
||||
/**
|
||||
* Ensure that export_user_preferences returns no data if the user has not visited the myoverview block.
|
||||
*/
|
||||
public function test_export_user_preferences_no_pref() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
provider::export_user_preferences($user->id);
|
||||
$writer = writer::with_context(\context_system::instance());
|
||||
$this->assertFalse($writer->has_any_data());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the preference courses is exported properly.
|
||||
*/
|
||||
public function test_export_user_preferences_course_preference() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
set_user_preference('block_myoverview_last_tab', 'courses', $user);
|
||||
|
||||
provider::export_user_preferences($user->id);
|
||||
$writer = writer::with_context(\context_system::instance());
|
||||
$blockpreferences = $writer->get_user_preferences('block_myoverview');
|
||||
$this->assertEquals('courses', $blockpreferences->block_myoverview_last_tab->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the preference timeline is exported properly.
|
||||
*/
|
||||
public function test_export_user_preferences_timeline_preference() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
set_user_preference('block_myoverview_last_tab', 'timeline', $user);
|
||||
|
||||
provider::export_user_preferences($user->id);
|
||||
$writer = writer::with_context(\context_system::instance());
|
||||
$blockpreferences = $writer->get_user_preferences('block_myoverview');
|
||||
$this->assertEquals('timeline', $blockpreferences->block_myoverview_last_tab->value);
|
||||
}
|
||||
}
|
|
@ -24,6 +24,6 @@
|
|||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2018051400; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2018092700; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2018050800; // Requires this Moodle version.
|
||||
$plugin->component = 'block_myoverview'; // Full name of the plugin (used for diagnostics).
|
||||
|
|
1
blocks/timeline/amd/build/event_list.min.js
vendored
Normal file
1
blocks/timeline/amd/build/event_list.min.js
vendored
Normal file
|
@ -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:a<b,dayTimestamp:a,events:e})}),d},p=function(a,b){var d=o(a,b),e=j.EVENT_LIST_CONTENT;return c.render(e,d)},q=function(a,b,c,d,e,f){var i=a+c*h,j=void 0!=d&&a+d*h,k={starttime:i,limit:b};return e&&(k.aftereventid=e),j&&(k.endtime=j),f?(k.courseid=f,g.queryByCourse(k)):g.queryByTime(k)},r=function(a,b,c,d,e,f,g,h){for(var i=a.pageNumber,j=a.limit,k=i;!d.hasOwnProperty(k);)k--;var l=d[k],m=null;return m=e&&e.hasOwnProperty(i)?e[i]:q(c,j+1,g,h,l,f),m.then(function(a){if(!a.events.length)return b.allItemsLoaded(i),[];var c=a.events,d=c.length<=j;return d?b.allItemsLoaded(i):c.pop(),c})},s=function(c,f,g,h,i,j,l,m){var n={1:0},o=!1,q=a.extend({},k);return e.get_string("ariaeventlistpagelimit","block_timeline",a.isArray(c)?c[0]:c).then(function(a){return q.ariaLabels.itemsperpage=a,q.ariaLabels.paginationnav=m,a}).then(function(){return d.createWithLimit(c,function(c,d){var e=[];return c.forEach(function(a){var c=a.pageNumber,h=r(a,d,g,n,f,i,j,l).then(function(a){if(a.length){o=!0;var b=a[a.length-1].id;return n[c+1]=b,p(a,g)}return a})["catch"](b.exception);e.push(h)}),a.when.apply(a,e).then(function(){h.resolve(o)})["catch"](function(){h.resolve(o)}),e},q)})},t=function(d,e,f,g){d=a(d);var h=a.Deferred(),j=d.find(i.EVENT_LIST_CONTENT),k=d.find(i.EVENT_LIST_LOADING_PLACEHOLDER),o=d.attr("data-course-id"),p=parseInt(d.attr("data-days-offset"),10),q=d.attr("data-days-limit"),r=parseInt(d.attr("data-midnight"),10);n(d),m(d),k.removeClass("hidden"),void 0!=q&&(q=parseInt(q,10)),s(e,f,r,h,o,p,q,g).then(function(b,e){return b=a(b),b.addClass("hidden"),c.replaceNodeContents(j,b,e),h.then(function(a){return b.removeClass("hidden"),k.addClass("hidden"),a||l(d),a})["catch"](function(){return!1}),b})["catch"](b.exception)};return{init:t,rootSelector:i.ROOT}});
|
1
blocks/timeline/amd/build/main.min.js
vendored
Normal file
1
blocks/timeline/amd/build/main.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
define(["jquery","block_timeline/view_nav","block_timeline/view"],function(a,b,c){var d={TIMELINE_VIEW:'[data-region="timeline-view"]'},e=function(e){e=a(e);var f=e.find(d.TIMELINE_VIEW);b.init(e,f),c.init(f)};return{init:e}});
|
1
blocks/timeline/amd/build/paging_bar.min.js
vendored
Normal file
1
blocks/timeline/amd/build/paging_bar.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
define(["jquery","core/custom_interaction_events"],function(a,b){var c={ROOT:'[data-region="paging-bar"]',PAGE_ITEM:'[data-region="page-item"]',ACTIVE_PAGE_ITEM:'[data-region="page-item"].active'},d={PAGE_SELECTED:"block_myoverview-paging-bar-page-selected"},e=function(a,b){return a.find(c.PAGE_ITEM+'[data-page-number="'+b+'"]')},f=function(a,b){var c=b.attr("data-page-number");return"first"==c?c=1:"last"==c&&(c=a.attr("data-page-count")),c},g=function(g){g=a(g),b.define(g,[b.events.activate]),g.on(b.events.activate,c.PAGE_ITEM,function(b,h){var i=a(b.target).closest(c.PAGE_ITEM),j=g.find(c.ACTIVE_PAGE_ITEM),k=f(g,i),l=k==f(g,j);l||(g.find(c.PAGE_ITEM).removeClass("active"),e(g,k).addClass("active")),g.trigger(d.PAGE_SELECTED,[{pageNumber:k,isSamePage:l}]),h.originalEvent.preventDefault()})};return{registerEventListeners:g,events:d,rootSelector:c.ROOT}});
|
1
blocks/timeline/amd/build/paging_content.min.js
vendored
Normal file
1
blocks/timeline/amd/build/paging_content.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
define(["jquery","core/templates","block_myoverview/paging_bar"],function(a,b,c){var d={ROOT:'[data-region="paging-content"]',PAGE_REGION:'[data-region="paging-content-item"]'},e=function(b,c){this.root=a(b),this.pagingBar=a(c)};return e.rootSelector=d.ROOT,e.prototype.createPage=function(a){return this.loadContent(a).then(function(a,c){b.appendNodeContents(this.root,a,c)}.bind(this)).then(function(){return this.findPage(a)}.bind(this))},e.prototype.findPage=function(a){return this.root.find('[data-page="'+a+'"]')},e.prototype.showPage=function(a){var b=this.findPage(a);this.root.find(d.PAGE_REGION).addClass("hidden"),b.length?b.removeClass("hidden"):this.createPage(a).done(function(a){a.removeClass("hidden")})},e.prototype.registerEventListeners=function(){this.pagingBar.on(c.events.PAGE_SELECTED,function(a,b){b.isSamePage||this.showPage(b.pageNumber)}.bind(this))},e});
|
1
blocks/timeline/amd/build/view.min.js
vendored
Normal file
1
blocks/timeline/amd/build/view.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
define(["jquery","block_timeline/view_dates","block_timeline/view_courses"],function(a,b,c){var d={TIMELINE_DATES_VIEW:'[data-region="view-dates"]',TIMELINE_COURSES_VIEW:'[data-region="view-courses"]'},e=function(e){e=a(e);var f=e.find(d.TIMELINE_DATES_VIEW),g=e.find(d.TIMELINE_COURSES_VIEW);b.init(f),c.init(g)},f=function(a){var e=a.find(d.TIMELINE_DATES_VIEW),f=a.find(d.TIMELINE_COURSES_VIEW);b.reset(e),c.reset(f)},g=function(a){var e=a.find(d.TIMELINE_DATES_VIEW),f=a.find(d.TIMELINE_COURSES_VIEW);e.hasClass("active")?b.shown(e):c.shown(f)};return{init:e,reset:f,shown:g}});
|
1
blocks/timeline/amd/build/view_courses.min.js
vendored
Normal file
1
blocks/timeline/amd/build/view_courses.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
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(a,b,c,d,e,f,g,h){var i={MORE_COURSES_BUTTON:'[data-action="more-courses"]',MORE_COURSES_BUTTON_CONTAINER:'[data-region="more-courses-button-container"]',NO_COURSES_EMPTY_MESSAGE:'[data-region="no-courses-empty-message"]',COURSES_LIST:'[data-region="courses-list"]',COURSE_ITEMS_LOADING_PLACEHOLDER:'[data-region="course-items-loading-placeholder"]',COURSE_EVENTS_CONTAINER:'[data-region="course-events-container"]',COURSE_NAME:'[data-region="course-name"]',LOADING_ICON:".loading-icon"},j={COURSE_ITEMS:"block_timeline/course-items",LOADING_ICON:"core/loading"},k="inprogress",l="fullname asc",m=5,n=2,o=86400,p=function(a){a.find(i.COURSE_ITEMS_LOADING_PLACEHOLDER).addClass("hidden")},q=function(a){a.find(i.MORE_COURSES_BUTTON_CONTAINER).addClass("hidden")},r=function(a){a.find(i.MORE_COURSES_BUTTON_CONTAINER).removeClass("hidden")},s=function(a){var b=a.find(i.MORE_COURSES_BUTTON);b.prop("disabled",!0),e.render(j.LOADING_ICON,{}).then(function(a){return b.append(a),a})["catch"](function(){return!1})},t=function(a){var b=a.find(i.MORE_COURSES_BUTTON);b.prop("disabled",!1),b.find(i.LOADING_ICON).remove()},u=function(a){a.find(i.NO_COURSES_EMPTY_MESSAGE).removeClass("hidden")},v=function(a,b){var c=a.find(i.COURSES_LIST);e.appendNodeContents(c,b,"")},w=function(a){return a.find(i.COURSE_EVENTS_CONTAINER).length>0},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<n?q(b):r(b),c})["catch"](function(){p(b)})},L=function(c){var e=x(c),h=z(c);return g.getEnrolledCoursesByTimelineClassification(k,h,e,l).then(function(b){var e=Date.now(),g=b.courses,h=b.nextoffset,i=A(c),j=B(c),k=C(c),l=D(c),n=E(c),o=c.attr("data-no-events-url");y(c,h);var p=J(g,l,n),q=K(g,c,k,i,j,o);return a.when(p,q).then(function(b){return I(c,e)?b:(g.forEach(function(e){var g=e.id,h=[],i='[data-region="course-events-container"][data-course-id="'+g+'"]',j=c.find(i),k=j.find(f.rootSelector),l=b.groupedbycourse.filter(function(a){return a.courseid==g});l.length&&(h=l[0].events);var n=a.Deferred().resolve({events:h}).promise();d.get_string("ariaeventlistpaginationnavcourses","block_timeline",e.fullnamedisplay).then(function(a){return f.init(k,m,{1:n},a),a})["catch"](function(){f.init(k,m,{1:n})})}),b)})})["catch"](b.exception)},M=function(c){var e=Date.now(),g=D(c),h=E(c),j=c.find(i.COURSE_EVENTS_CONTAINER),k=j.map(function(){return a(this).attr("data-course-id")}).get();return H(c,e),F(k,g,m+1,h).then(function(b){return I(c,e)?b:(j.each(function(c,e){e=a(e);var g=e.attr("data-course-id"),h=e.find(i.COURSE_NAME).text(),j=e.find(f.rootSelector),k=a.Deferred(),l=[],n=b.groupedbycourse.filter(function(a){return a.courseid==g});n.length&&(l=n[0].events),k.resolve({events:l}),d.get_string("ariaeventlistpaginationnavcourses","block_timeline",h).then(function(a){return f.init(j,m,{1:k.promise()},a),a})["catch"](function(){f.init(j,m,{1:k.promise()})})}),b)})["catch"](b.exception)},N=function(a){c.define(a,[c.events.activate]),a.on(c.events.activate,i.MORE_COURSES_BUTTON,function(b,c){s(a),L(a).then(function(){t(a)})["catch"](function(){t(a)}),c&&(c.originalEvent.preventDefault(),c.originalEvent.stopPropagation()),b.stopPropagation()})},O=function(b){b=a(b),H(b,Date.now()),b.hasClass("active")&&(L(b),b.attr("data-seen",!0)),N(b)},P=function(a){a.removeAttr("data-seen"),a.hasClass("active")&&Q(a)},Q=function(a){a.attr("data-seen")||(w(a)?M(a):L(a),a.attr("data-seen",!0))};return{init:O,reset:P,shown:Q}});
|
1
blocks/timeline/amd/build/view_dates.min.js
vendored
Normal file
1
blocks/timeline/amd/build/view_dates.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
define(["jquery","core/str","block_timeline/event_list"],function(a,b,c){var d={EVENT_LIST_CONTAINER:'[data-region="event-list-container"]'},e=function(a){var e=a.find(d.EVENT_LIST_CONTAINER);b.get_string("ariaeventlistpaginationnavdates","block_timeline").then(function(a){return c.init(e,[5,10,25],{},a),a})["catch"](function(){c.init(e,[5,10,25])})},f=function(b){b=a(b),b.hasClass("active")&&(e(b),b.attr("data-seen",!0))},g=function(a){a.removeAttr("data-seen"),a.hasClass("active")&&(e(a),a.attr("data-seen",!0))},h=function(a){a.attr("data-seen")||(e(a),a.attr("data-seen",!0))};return{init:f,reset:g,shown:h}});
|
1
blocks/timeline/amd/build/view_nav.min.js
vendored
Normal file
1
blocks/timeline/amd/build/view_nav.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
define(["jquery","core/custom_interaction_events","block_timeline/view"],function(a,b,c){var d={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]"},e=function(e,f){var g=e.find(d.TIMELINE_DAY_FILTER);b.define(g,[b.events.activate]),g.on(b.events.activate,d.TIMELINE_DAY_FILTER_OPTION,function(b,g){var h=a(b.target).closest(d.TIMELINE_DAY_FILTER_OPTION);if(!h.hasClass("active")){var i=h.attr("data-from"),j=h.attr("data-to"),k=e.find(d.DATA_DAYS_OFFSET);k.attr("data-days-offset",i),void 0!=j?k.attr("data-days-limit",j):k.removeAttr("data-days-limit"),c.reset(f),g.originalEvent.preventDefault()}})},f=function(a,b){a.find(d.TIMELINE_VIEW_SELECTOR).on("shown shown.bs.tab",function(){c.shown(b)})},g=function(b,c){b=a(b),e(b,c),f(b,c)};return{init:g}});
|
|
@ -16,10 +16,8 @@
|
|||
/**
|
||||
* A javascript module to retrieve calendar events from the server.
|
||||
*
|
||||
* @module block_myoverview/calendar_events_repository
|
||||
* @class repository
|
||||
* @package block_myoverview
|
||||
* @copyright 2016 Ryan Wyllie <ryan@moodle.com>
|
||||
* @module block_timeline/calendar_events_repository
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) {
|
465
blocks/timeline/amd/src/event_list.js
Normal file
465
blocks/timeline/amd/src/event_list.js
Normal file
|
@ -0,0 +1,465 @@
|
|||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Javascript to load and render the list of calendar events for a
|
||||
* given day range.
|
||||
*
|
||||
* @module block_timeline/event_list
|
||||
* @package block_timeline
|
||||
* @copyright 2016 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'core/notification',
|
||||
'core/templates',
|
||||
'core/paged_content_factory',
|
||||
'core/str',
|
||||
'core/user_date',
|
||||
'block_timeline/calendar_events_repository'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
Notification,
|
||||
Templates,
|
||||
PagedContentFactory,
|
||||
Str,
|
||||
UserDate,
|
||||
CalendarEventsRepository
|
||||
) {
|
||||
|
||||
var SECONDS_IN_DAY = 60 * 60 * 24;
|
||||
|
||||
var SELECTORS = {
|
||||
EMPTY_MESSAGE: '[data-region="empty-message"]',
|
||||
ROOT: '[data-region="event-list-container"]',
|
||||
EVENT_LIST_CONTENT: '[data-region="event-list-content"]',
|
||||
EVENT_LIST_LOADING_PLACEHOLDER: '[data-region="event-list-loading-placeholder"]',
|
||||
};
|
||||
|
||||
var TEMPLATES = {
|
||||
EVENT_LIST_CONTENT: 'block_timeline/event-list-content'
|
||||
};
|
||||
|
||||
// We want the paged content controls below the paged content area
|
||||
// and the controls should be ignored while data is loading.
|
||||
var DEFAULT_PAGED_CONTENT_CONFIG = {
|
||||
ignoreControlWhileLoading: true,
|
||||
controlPlacementBottom: true,
|
||||
ariaLabels: {
|
||||
itemsperpagecomponents: 'ariaeventlistpagelimit, block_timeline',
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Hide the content area and display the empty content message.
|
||||
*
|
||||
* @param {object} root The container element
|
||||
*/
|
||||
var hideContent = function(root) {
|
||||
root.find(SELECTORS.EVENT_LIST_CONTENT).addClass('hidden');
|
||||
root.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden');
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the content area and hide the empty content message.
|
||||
*
|
||||
* @param {object} root The container element
|
||||
*/
|
||||
var showContent = function(root) {
|
||||
root.find(SELECTORS.EVENT_LIST_CONTENT).removeClass('hidden');
|
||||
root.find(SELECTORS.EMPTY_MESSAGE).addClass('hidden');
|
||||
};
|
||||
|
||||
/**
|
||||
* Empty the content area.
|
||||
*
|
||||
* @param {object} root The container element
|
||||
*/
|
||||
var emptyContent = function(root) {
|
||||
root.find(SELECTORS.EVENT_LIST_CONTENT).empty();
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct the template context from a list of calendar events. The events
|
||||
* are grouped by which day they are on. The day is calculated from the user's
|
||||
* midnight timestamp to ensure that the calculation is timezone agnostic.
|
||||
*
|
||||
* The return data structure will look like:
|
||||
* {
|
||||
* eventsbyday: [
|
||||
* {
|
||||
* dayTimestamp: 1533744000,
|
||||
* events: [
|
||||
* { ...event 1 data... },
|
||||
* { ...event 2 data... }
|
||||
* ]
|
||||
* },
|
||||
* {
|
||||
* dayTimestamp: 1533830400,
|
||||
* events: [
|
||||
* { ...event 3 data... },
|
||||
* { ...event 4 data... }
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* Each day timestamp is the day's midnight in the user's timezone.
|
||||
*
|
||||
* @param {array} calendarEvents List of calendar events
|
||||
* @param {Number} midnight A timestamp representing midnight in the user's timezone
|
||||
* @return {object}
|
||||
*/
|
||||
var buildTemplateContext = function(calendarEvents, midnight) {
|
||||
var eventsByDay = {};
|
||||
var templateContext = {
|
||||
eventsbyday: []
|
||||
};
|
||||
|
||||
calendarEvents.forEach(function(calendarEvent) {
|
||||
var dayTimestamp = UserDate.getUserMidnightForTimestamp(calendarEvent.timesort, midnight);
|
||||
if (eventsByDay[dayTimestamp]) {
|
||||
eventsByDay[dayTimestamp].push(calendarEvent);
|
||||
} else {
|
||||
eventsByDay[dayTimestamp] = [calendarEvent];
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(eventsByDay).forEach(function(dayTimestamp) {
|
||||
var events = eventsByDay[dayTimestamp];
|
||||
templateContext.eventsbyday.push({
|
||||
past: dayTimestamp < midnight,
|
||||
dayTimestamp: dayTimestamp,
|
||||
events: events
|
||||
});
|
||||
});
|
||||
|
||||
return templateContext;
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the HTML for the given calendar events.
|
||||
*
|
||||
* @param {array} calendarEvents A list of calendar events
|
||||
* @param {Number} midnight A timestamp representing midnight for the user
|
||||
* @return {promise} Resolved with HTML and JS strings.
|
||||
*/
|
||||
var render = function(calendarEvents, midnight) {
|
||||
var templateContext = buildTemplateContext(calendarEvents, midnight);
|
||||
var templateName = TEMPLATES.EVENT_LIST_CONTENT;
|
||||
|
||||
return Templates.render(templateName, templateContext);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve a list of calendar events from the server for the given
|
||||
* constraints.
|
||||
*
|
||||
* @param {Number} midnight The user's midnight time in unix timestamp.
|
||||
* @param {Number} limit Limit the result set to this number of items
|
||||
* @param {Number} daysOffset How many days (from midnight) to offset the results from
|
||||
* @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to
|
||||
* @param {int|falsey} lastId The ID of the last seen event (if any)
|
||||
* @param {int|undefined} courseId Course ID to restrict events to
|
||||
* @return {promise} A jquery promise
|
||||
*/
|
||||
var load = function(midnight, limit, daysOffset, daysLimit, lastId, courseId) {
|
||||
var startTime = midnight + (daysOffset * SECONDS_IN_DAY);
|
||||
var endTime = daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : false;
|
||||
|
||||
var args = {
|
||||
starttime: startTime,
|
||||
limit: limit,
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
args.aftereventid = lastId;
|
||||
}
|
||||
|
||||
if (endTime) {
|
||||
args.endtime = endTime;
|
||||
}
|
||||
|
||||
if (courseId) {
|
||||
// If we have a course id then we only want events from that course.
|
||||
args.courseid = courseId;
|
||||
return CalendarEventsRepository.queryByCourse(args);
|
||||
} else {
|
||||
// Otherwise we want events from any course.
|
||||
return CalendarEventsRepository.queryByTime(args);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a single page request from the paged content. Uses the given page data to request
|
||||
* the events from the server.
|
||||
*
|
||||
* Checks the given preloadedPages before sending a request to the server to make sure we
|
||||
* don't load data unnecessarily.
|
||||
*
|
||||
* @param {object} pageData A single page data (see core/paged_content_pages for more info).
|
||||
* @param {object} actions Paged content actions (see core/paged_content_pages for more info).
|
||||
* @param {Number} midnight The user's midnight time in unix timestamp.
|
||||
* @param {object} lastIds The last event ID for each loaded page. Page number is key, id is value.
|
||||
* @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.
|
||||
* @param {int|undefined} courseId Course ID to restrict events to
|
||||
* @param {Number} daysOffset How many days (from midnight) to offset the results from
|
||||
* @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to
|
||||
* @return {object} jQuery promise resolved with calendar events.
|
||||
*/
|
||||
var loadEventsFromPageData = function(
|
||||
pageData,
|
||||
actions,
|
||||
midnight,
|
||||
lastIds,
|
||||
preloadedPages,
|
||||
courseId,
|
||||
daysOffset,
|
||||
daysLimit
|
||||
) {
|
||||
var pageNumber = pageData.pageNumber;
|
||||
var limit = pageData.limit;
|
||||
var lastPageNumber = pageNumber;
|
||||
|
||||
// This is here to protect us if, for some reason, the pages
|
||||
// are loaded out of order somehow and we don't have a reference
|
||||
// to the previous page. In that case, scan back to find the most
|
||||
// recent page we've seen.
|
||||
while (!lastIds.hasOwnProperty(lastPageNumber)) {
|
||||
lastPageNumber--;
|
||||
}
|
||||
// Use the last id of the most recent page.
|
||||
var lastId = lastIds[lastPageNumber];
|
||||
var eventsPromise = null;
|
||||
|
||||
if (preloadedPages && preloadedPages.hasOwnProperty(pageNumber)) {
|
||||
// This page has been preloaded so use that rather than load the values
|
||||
// again.
|
||||
eventsPromise = preloadedPages[pageNumber];
|
||||
} else {
|
||||
// Load one more than the given limit so that we can tell if there
|
||||
// is more content to load after this.
|
||||
eventsPromise = load(midnight, limit + 1, daysOffset, daysLimit, lastId, courseId);
|
||||
}
|
||||
|
||||
return eventsPromise.then(function(result) {
|
||||
if (!result.events.length) {
|
||||
// If we didn't get any events back then tell the paged content
|
||||
// that we're done loading.
|
||||
actions.allItemsLoaded(pageNumber);
|
||||
return [];
|
||||
}
|
||||
|
||||
var calendarEvents = result.events;
|
||||
// We expect to receive limit + 1 events back from the server.
|
||||
// Any less means there are no more events to load.
|
||||
var loadedAll = calendarEvents.length <= limit;
|
||||
|
||||
if (loadedAll) {
|
||||
// Tell the pagination that everything is loaded.
|
||||
actions.allItemsLoaded(pageNumber);
|
||||
} else {
|
||||
// Remove the last element from the array because it isn't
|
||||
// needed in this result set.
|
||||
calendarEvents.pop();
|
||||
}
|
||||
|
||||
return calendarEvents;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Use the paged content factory to create a paged content element for showing
|
||||
* the event list. We only provide a page limit to the factory because we don't
|
||||
* know exactly how many pages we'll need. This creates a paging bar with just
|
||||
* next/previous buttons.
|
||||
*
|
||||
* This function specifies the callback for loading the event data that the user
|
||||
* is requesting.
|
||||
*
|
||||
* @param {int|array} pageLimit A single limit or list of limits as options for the paged content
|
||||
* @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.
|
||||
* @param {Number} midnight The user's midnight time in unix timestamp.
|
||||
* @param {object} firstLoad A jQuery promise to be resolved after the first set of data is loaded.
|
||||
* @param {int|undefined} courseId Course ID to restrict events to
|
||||
* @param {Number} daysOffset How many days (from midnight) to offset the results from
|
||||
* @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to
|
||||
* @param {string} paginationAriaLabel String to set as the aria label for the pagination bar.
|
||||
* @return {object} jQuery promise.
|
||||
*/
|
||||
var createPagedContent = function(
|
||||
pageLimit,
|
||||
preloadedPages,
|
||||
midnight,
|
||||
firstLoad,
|
||||
courseId,
|
||||
daysOffset,
|
||||
daysLimit,
|
||||
paginationAriaLabel
|
||||
) {
|
||||
// Remember the last event id we loaded on each page because we can't
|
||||
// use the offset value since the backend can skip events if the user doesn't
|
||||
// have the capability to see them. Instead we load the next page of events
|
||||
// based on the last seen event id.
|
||||
var lastIds = {'1': 0};
|
||||
var hasContent = false;
|
||||
var config = $.extend({}, DEFAULT_PAGED_CONTENT_CONFIG);
|
||||
|
||||
return Str.get_string(
|
||||
'ariaeventlistpagelimit',
|
||||
'block_timeline',
|
||||
$.isArray(pageLimit) ? pageLimit[0] : pageLimit
|
||||
)
|
||||
.then(function(string) {
|
||||
config.ariaLabels.itemsperpage = string;
|
||||
config.ariaLabels.paginationnav = paginationAriaLabel;
|
||||
return string;
|
||||
})
|
||||
.then(function() {
|
||||
return PagedContentFactory.createWithLimit(
|
||||
pageLimit,
|
||||
function(pagesData, actions) {
|
||||
var promises = [];
|
||||
|
||||
pagesData.forEach(function(pageData) {
|
||||
var pageNumber = pageData.pageNumber;
|
||||
// Load the page data.
|
||||
var pagePromise = loadEventsFromPageData(
|
||||
pageData,
|
||||
actions,
|
||||
midnight,
|
||||
lastIds,
|
||||
preloadedPages,
|
||||
courseId,
|
||||
daysOffset,
|
||||
daysLimit
|
||||
).then(function(calendarEvents) {
|
||||
if (calendarEvents.length) {
|
||||
// Remember that we've loaded content.
|
||||
hasContent = true;
|
||||
// Remember the last id we've seen.
|
||||
var lastEventId = calendarEvents[calendarEvents.length - 1].id;
|
||||
// Record the id that the next page will need to start from.
|
||||
lastIds[pageNumber + 1] = lastEventId;
|
||||
// Get the HTML and JS for these calendar events.
|
||||
return render(calendarEvents, midnight);
|
||||
} else {
|
||||
return calendarEvents;
|
||||
}
|
||||
})
|
||||
.catch(Notification.exception);
|
||||
|
||||
promises.push(pagePromise);
|
||||
});
|
||||
|
||||
$.when.apply($, promises).then(function() {
|
||||
// Tell the calling code that the first page has been loaded
|
||||
// and whether it contains any content.
|
||||
firstLoad.resolve(hasContent);
|
||||
return;
|
||||
})
|
||||
.catch(function() {
|
||||
firstLoad.resolve(hasContent);
|
||||
});
|
||||
|
||||
return promises;
|
||||
},
|
||||
config
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a paged content region for the calendar events in the given root element.
|
||||
* The content of the root element are replaced with a new paged content section
|
||||
* each time this function is called.
|
||||
*
|
||||
* This function will be called each time the offset or limit values are changed to
|
||||
* reload the event list region.
|
||||
*
|
||||
* @param {object} root The event list container element
|
||||
* @param {int|array} pageLimit A single limit or list of limits as options for the paged content
|
||||
* @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.
|
||||
* @param {string} paginationAriaLabel String to set as the aria label for the pagination bar.
|
||||
*/
|
||||
var init = function(root, pageLimit, preloadedPages, paginationAriaLabel) {
|
||||
root = $(root);
|
||||
|
||||
// Create a promise that will be resolved once the first set of page
|
||||
// data has been loaded. This ensures that the loading placeholder isn't
|
||||
// hidden until we have all of the data back to prevent the page elements
|
||||
// jumping around.
|
||||
var firstLoad = $.Deferred();
|
||||
var eventListContent = root.find(SELECTORS.EVENT_LIST_CONTENT);
|
||||
var loadingPlaceholder = root.find(SELECTORS.EVENT_LIST_LOADING_PLACEHOLDER);
|
||||
var courseId = root.attr('data-course-id');
|
||||
var daysOffset = parseInt(root.attr('data-days-offset'), 10);
|
||||
var daysLimit = root.attr('data-days-limit');
|
||||
var midnight = parseInt(root.attr('data-midnight'), 10);
|
||||
|
||||
// Make sure the content area and loading placeholder is visible.
|
||||
// This is because the init function can be called to re-initialise
|
||||
// an existing event list area.
|
||||
emptyContent(root);
|
||||
showContent(root);
|
||||
loadingPlaceholder.removeClass('hidden');
|
||||
|
||||
// Days limit isn't mandatory.
|
||||
if (daysLimit != undefined) {
|
||||
daysLimit = parseInt(daysLimit, 10);
|
||||
}
|
||||
|
||||
// Created the paged content element.
|
||||
createPagedContent(pageLimit, preloadedPages, midnight, firstLoad, courseId, daysOffset, daysLimit, paginationAriaLabel)
|
||||
.then(function(html, js) {
|
||||
html = $(html);
|
||||
// Hide the content for now.
|
||||
html.addClass('hidden');
|
||||
// Replace existing elements with the newly created paged content.
|
||||
// If we're reinitialising an existing event list this will replace
|
||||
// the old event list (including removing any event handlers).
|
||||
Templates.replaceNodeContents(eventListContent, html, js);
|
||||
|
||||
firstLoad.then(function(hasContent) {
|
||||
// Prevent changing page elements too much by only showing the content
|
||||
// once we've loaded some data for the first time. This allows our
|
||||
// fancy loading placeholder to shine.
|
||||
html.removeClass('hidden');
|
||||
loadingPlaceholder.addClass('hidden');
|
||||
|
||||
if (!hasContent) {
|
||||
// If we didn't get any data then show the empty data message.
|
||||
hideContent(root);
|
||||
}
|
||||
|
||||
return hasContent;
|
||||
})
|
||||
.catch(function() {
|
||||
return false;
|
||||
});
|
||||
|
||||
return html;
|
||||
})
|
||||
.catch(Notification.exception);
|
||||
};
|
||||
|
||||
return {
|
||||
init: init,
|
||||
rootSelector: SELECTORS.ROOT,
|
||||
};
|
||||
});
|
57
blocks/timeline/amd/src/main.js
Normal file
57
blocks/timeline/amd/src/main.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Javascript to initialise the timeline block.
|
||||
*
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'block_timeline/view_nav',
|
||||
'block_timeline/view'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
ViewNav,
|
||||
View
|
||||
) {
|
||||
|
||||
var SELECTORS = {
|
||||
TIMELINE_VIEW: '[data-region="timeline-view"]'
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialise all of the modules for the timeline block.
|
||||
*
|
||||
* @param {object} root The root element for the timeline block.
|
||||
*/
|
||||
var init = function(root) {
|
||||
root = $(root);
|
||||
var viewRoot = root.find(SELECTORS.TIMELINE_VIEW);
|
||||
|
||||
// Initialise the timeline navigation elements.
|
||||
ViewNav.init(root, viewRoot);
|
||||
// Initialise the timeline view modules.
|
||||
View.init(viewRoot);
|
||||
};
|
||||
|
||||
return {
|
||||
init: init
|
||||
};
|
||||
});
|
97
blocks/timeline/amd/src/view.js
Normal file
97
blocks/timeline/amd/src/view.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Manage the timeline view for the timeline block.
|
||||
*
|
||||
* @package block_timeline
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'block_timeline/view_dates',
|
||||
'block_timeline/view_courses',
|
||||
],
|
||||
function(
|
||||
$,
|
||||
ViewDates,
|
||||
ViewCourses
|
||||
) {
|
||||
|
||||
var SELECTORS = {
|
||||
TIMELINE_DATES_VIEW: '[data-region="view-dates"]',
|
||||
TIMELINE_COURSES_VIEW: '[data-region="view-courses"]',
|
||||
};
|
||||
|
||||
/**
|
||||
* Intialise the timeline dates and courses views on page load.
|
||||
* This function should only be called once per page load because
|
||||
* it can cause event listeners to be added to the page.
|
||||
*
|
||||
* @param {object} root The root element for the timeline view.
|
||||
*/
|
||||
var init = function(root) {
|
||||
root = $(root);
|
||||
var datesViewRoot = root.find(SELECTORS.TIMELINE_DATES_VIEW);
|
||||
var coursesViewRoot = root.find(SELECTORS.TIMELINE_COURSES_VIEW);
|
||||
|
||||
ViewDates.init(datesViewRoot);
|
||||
ViewCourses.init(coursesViewRoot);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the timeline dates and courses views to their original
|
||||
* state on first page load.
|
||||
*
|
||||
* This is called when configuration has changed for the event lists
|
||||
* to cause them to reload their data.
|
||||
*
|
||||
* @param {object} root The root element for the timeline view.
|
||||
*/
|
||||
var reset = function(root) {
|
||||
var datesViewRoot = root.find(SELECTORS.TIMELINE_DATES_VIEW);
|
||||
var coursesViewRoot = root.find(SELECTORS.TIMELINE_COURSES_VIEW);
|
||||
ViewDates.reset(datesViewRoot);
|
||||
ViewCourses.reset(coursesViewRoot);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tell the timeline dates or courses view that it has been displayed.
|
||||
*
|
||||
* This is called each time one of the views is displayed and is used to
|
||||
* lazy load the data within it on first load.
|
||||
*
|
||||
* @param {object} root The root element for the timeline view.
|
||||
*/
|
||||
var shown = function(root) {
|
||||
var datesViewRoot = root.find(SELECTORS.TIMELINE_DATES_VIEW);
|
||||
var coursesViewRoot = root.find(SELECTORS.TIMELINE_COURSES_VIEW);
|
||||
|
||||
if (datesViewRoot.hasClass('active')) {
|
||||
ViewDates.shown(datesViewRoot);
|
||||
} else {
|
||||
ViewCourses.shown(coursesViewRoot);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
init: init,
|
||||
reset: reset,
|
||||
shown: shown,
|
||||
};
|
||||
});
|
606
blocks/timeline/amd/src/view_courses.js
Normal file
606
blocks/timeline/amd/src/view_courses.js
Normal file
|
@ -0,0 +1,606 @@
|
|||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Manage the timeline courses view for the timeline block.
|
||||
*
|
||||
* @package block_timeline
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'core/notification',
|
||||
'core/custom_interaction_events',
|
||||
'core/str',
|
||||
'core/templates',
|
||||
'block_timeline/event_list',
|
||||
'core_course/repository',
|
||||
'block_timeline/calendar_events_repository'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
Notification,
|
||||
CustomEvents,
|
||||
Str,
|
||||
Templates,
|
||||
EventList,
|
||||
CourseRepository,
|
||||
EventsRepository
|
||||
) {
|
||||
|
||||
var SELECTORS = {
|
||||
MORE_COURSES_BUTTON: '[data-action="more-courses"]',
|
||||
MORE_COURSES_BUTTON_CONTAINER: '[data-region="more-courses-button-container"]',
|
||||
NO_COURSES_EMPTY_MESSAGE: '[data-region="no-courses-empty-message"]',
|
||||
COURSES_LIST: '[data-region="courses-list"]',
|
||||
COURSE_ITEMS_LOADING_PLACEHOLDER: '[data-region="course-items-loading-placeholder"]',
|
||||
COURSE_EVENTS_CONTAINER: '[data-region="course-events-container"]',
|
||||
COURSE_NAME: '[data-region="course-name"]',
|
||||
LOADING_ICON: '.loading-icon'
|
||||
};
|
||||
|
||||
var TEMPLATES = {
|
||||
COURSE_ITEMS: 'block_timeline/course-items',
|
||||
LOADING_ICON: 'core/loading'
|
||||
};
|
||||
|
||||
var COURSE_CLASSIFICATION = 'inprogress';
|
||||
var COURSE_SORT = 'fullname asc';
|
||||
var COURSE_EVENT_LIMIT = 5;
|
||||
var COURSE_LIMIT = 2;
|
||||
var SECONDS_IN_DAY = 60 * 60 * 24;
|
||||
|
||||
/**
|
||||
* Hide the loading placeholder elements.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
*/
|
||||
var hideLoadingPlaceholder = function(root) {
|
||||
root.find(SELECTORS.COURSE_ITEMS_LOADING_PLACEHOLDER).addClass('hidden');
|
||||
};
|
||||
|
||||
/**
|
||||
* Hide the "more courses" button.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
*/
|
||||
var hideMoreCoursesButton = function(root) {
|
||||
root.find(SELECTORS.MORE_COURSES_BUTTON_CONTAINER).addClass('hidden');
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the "more courses" button.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
*/
|
||||
var showMoreCoursesButton = function(root) {
|
||||
root.find(SELECTORS.MORE_COURSES_BUTTON_CONTAINER).removeClass('hidden');
|
||||
};
|
||||
|
||||
/**
|
||||
* Disable the "more courses" button and show the loading spinner.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
*/
|
||||
var enableMoreCoursesButtonLoading = function(root) {
|
||||
var button = root.find(SELECTORS.MORE_COURSES_BUTTON);
|
||||
button.prop('disabled', true);
|
||||
Templates.render(TEMPLATES.LOADING_ICON, {})
|
||||
.then(function(html) {
|
||||
button.append(html);
|
||||
return html;
|
||||
})
|
||||
.catch(function() {
|
||||
// It's not important if this false so just do so silently.
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable the "more courses" button and remove the loading spinner.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
*/
|
||||
var disableMoreCoursesButtonLoading = function(root) {
|
||||
var button = root.find(SELECTORS.MORE_COURSES_BUTTON);
|
||||
button.prop('disabled', false);
|
||||
button.find(SELECTORS.LOADING_ICON).remove();
|
||||
};
|
||||
|
||||
/**
|
||||
* Display the message for when there are no courses available.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
*/
|
||||
var showNoCoursesEmptyMessage = function(root) {
|
||||
root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).removeClass('hidden');
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the course items HTML to the page.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
* @param {string} html The course items HTML to render.
|
||||
*/
|
||||
var renderCourseItemsHTML = function(root, html) {
|
||||
var container = root.find(SELECTORS.COURSES_LIST);
|
||||
Templates.appendNodeContents(container, html, '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if any courses have been loaded.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
* @return {bool}
|
||||
*/
|
||||
var hasLoadedCourses = function(root) {
|
||||
return root.find(SELECTORS.COURSE_EVENTS_CONTAINER).length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the offset value for fetching courses.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
* @return {Number}
|
||||
*/
|
||||
var getOffset = function(root) {
|
||||
return parseInt(root.attr('data-offset'), 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the offset value for fetching courses.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
* @param {Number} offset Offset value.
|
||||
*/
|
||||
var setOffset = function(root, offset) {
|
||||
root.attr('data-offset', offset);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the limit value for fetching courses.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
* @return {Number}
|
||||
*/
|
||||
var getLimit = function(root) {
|
||||
return parseInt(root.attr('data-limit'), 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the days offset value for fetching events.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
* @return {Number}
|
||||
*/
|
||||
var getDaysOffset = function(root) {
|
||||
return parseInt(root.attr('data-days-offset'), 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the days limit value for fetching events. The days
|
||||
* limit is optional so undefined will be returned if it isn't
|
||||
* set.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
* @return {int|undefined}
|
||||
*/
|
||||
var getDaysLimit = function(root) {
|
||||
var daysLimit = root.attr('data-days-limit');
|
||||
return daysLimit != undefined ? parseInt(daysLimit, 10) : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the timestamp for the user's midnight.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
* @return {Number}
|
||||
*/
|
||||
var getMidnight = function(root) {
|
||||
return parseInt(root.attr('data-midnight'), 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the start time for fetching events. This is calculated
|
||||
* based on the user's midnight value so that timezones are
|
||||
* preserved.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
* @return {Number}
|
||||
*/
|
||||
var getStartTime = function(root) {
|
||||
var midnight = getMidnight(root);
|
||||
var daysOffset = getDaysOffset(root);
|
||||
return midnight + (daysOffset * SECONDS_IN_DAY);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the end time for fetching events. This is calculated
|
||||
* based on the user's midnight value so that timezones are
|
||||
* preserved.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
* @return {Number}
|
||||
*/
|
||||
var getEndTime = function(root) {
|
||||
var midnight = getMidnight(root);
|
||||
var daysLimit = getDaysLimit(root);
|
||||
return daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of events for the given course ids. Returns a promise that will
|
||||
* be resolved with the events.
|
||||
*
|
||||
* @param {array} courseIds The list of course ids to fetch events for.
|
||||
* @param {Number} startTime Timestamp to fetch events from.
|
||||
* @param {Number} limit Limit to the number of events (this applies per course, not total)
|
||||
* @param {Number} endTime Timestamp to fetch events to.
|
||||
* @return {object} jQuery promise.
|
||||
*/
|
||||
var getEventsForCourseIds = function(courseIds, startTime, limit, endTime) {
|
||||
var args = {
|
||||
courseids: courseIds,
|
||||
starttime: startTime,
|
||||
limit: limit
|
||||
};
|
||||
|
||||
if (endTime) {
|
||||
args.endtime = endTime;
|
||||
}
|
||||
|
||||
return EventsRepository.queryByCourses(args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the last time the events were reloaded.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
* @return {Number}
|
||||
*/
|
||||
var getEventReloadTime = function(root) {
|
||||
return root.data('last-event-load-time');
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the last time the events were reloaded.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
* @param {Number} time Timestamp in milliseconds.
|
||||
*/
|
||||
var setEventReloadTime = function(root, time) {
|
||||
root.data('last-event-load-time', time);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if events have begun reloading since the given
|
||||
* time.
|
||||
*
|
||||
* @param {object} root The rool element.
|
||||
* @param {Number} time Timestamp in milliseconds.
|
||||
* @return {bool}
|
||||
*/
|
||||
var hasReloadedEventsSince = function(root, time) {
|
||||
return getEventReloadTime(root) > time;
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a request to the server to load the events for the courses.
|
||||
*
|
||||
* @param {array} courses List of course objects.
|
||||
* @param {Number} startTime Timestamp to load events after.
|
||||
* @param {int|undefined} endTime Timestamp to load events up until.
|
||||
* @return {object} jQuery promise resolved with the events.
|
||||
*/
|
||||
var loadEventsForCourses = function(courses, startTime, endTime) {
|
||||
var courseIds = courses.map(function(course) {
|
||||
return course.id;
|
||||
});
|
||||
|
||||
return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime);
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the courses in the DOM once the server has returned the courses.
|
||||
*
|
||||
* @param {array} courses List of course objects.
|
||||
* @param {object} root The root element
|
||||
* @param {Number} midnight The midnight timestamp in the user's timezone.
|
||||
* @param {Number} daysOffset Number of days from today to offset the events.
|
||||
* @param {Number} daysLimit Number of days from today to limit the events to.
|
||||
* @param {string} noEventsURL URL for the image to display for no events.
|
||||
* @return {object} jQuery promise resolved after rendering is complete.
|
||||
*/
|
||||
var updateDisplayFromCourses = function(courses, root, midnight, daysOffset, daysLimit, noEventsURL) {
|
||||
// Render the courses template.
|
||||
return Templates.render(TEMPLATES.COURSE_ITEMS, {
|
||||
courses: courses,
|
||||
midnight: midnight,
|
||||
hasdaysoffset: true,
|
||||
hasdayslimit: daysLimit != undefined,
|
||||
daysoffset: daysOffset,
|
||||
dayslimit: daysLimit,
|
||||
nodayslimit: daysLimit == undefined,
|
||||
urls: {
|
||||
noevents: noEventsURL
|
||||
}
|
||||
}).then(function(html) {
|
||||
hideLoadingPlaceholder(root);
|
||||
|
||||
if (html) {
|
||||
// Template rendering is complete and we have the HTML so we can
|
||||
// add it to the DOM.
|
||||
renderCourseItemsHTML(root, html);
|
||||
} else {
|
||||
if (!hasLoadedCourses(root)) {
|
||||
// There were no courses to render so show the empty placeholder
|
||||
// message for the user to tell them.
|
||||
showNoCoursesEmptyMessage(root);
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
})
|
||||
.then(function(html) {
|
||||
if (courses.length < COURSE_LIMIT) {
|
||||
// We know there aren't any more courses because we got back less
|
||||
// than we asked for so hide the button to request more.
|
||||
hideMoreCoursesButton(root);
|
||||
} else {
|
||||
// Make sure the button is visible if there are more courses to load.
|
||||
showMoreCoursesButton(root);
|
||||
}
|
||||
|
||||
return html;
|
||||
})
|
||||
.catch(function() {
|
||||
hideLoadingPlaceholder(root);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Find all of the visible course blocks and initialise the event
|
||||
* list module to being loading the events for the course block.
|
||||
*
|
||||
* @param {object} root The root element for the timeline courses view.
|
||||
* @return {object} jQuery promise resolved with courses and events.
|
||||
*/
|
||||
var loadMoreCourses = function(root) {
|
||||
var offset = getOffset(root);
|
||||
var limit = getLimit(root);
|
||||
|
||||
// Start loading the next set of courses.
|
||||
return CourseRepository.getEnrolledCoursesByTimelineClassification(
|
||||
COURSE_CLASSIFICATION,
|
||||
limit,
|
||||
offset,
|
||||
COURSE_SORT
|
||||
).then(function(result) {
|
||||
var startEventLoadingTime = Date.now();
|
||||
var courses = result.courses;
|
||||
var nextOffset = result.nextoffset;
|
||||
var daysOffset = getDaysOffset(root);
|
||||
var daysLimit = getDaysLimit(root);
|
||||
var midnight = getMidnight(root);
|
||||
var startTime = getStartTime(root);
|
||||
var endTime = getEndTime(root);
|
||||
var noEventsURL = root.attr('data-no-events-url');
|
||||
// Record the next offset if we want to request more courses.
|
||||
setOffset(root, nextOffset);
|
||||
// Load the events for these courses.
|
||||
var eventsPromise = loadEventsForCourses(courses, startTime, endTime);
|
||||
// Render the courses in the DOM.
|
||||
var renderPromise = updateDisplayFromCourses(courses, root, midnight, daysOffset, daysLimit, noEventsURL);
|
||||
|
||||
return $.when(eventsPromise, renderPromise)
|
||||
.then(function(eventsByCourse) {
|
||||
if (hasReloadedEventsSince(root, startEventLoadingTime)) {
|
||||
// All of the events are being reloaded so ignore our results.
|
||||
return eventsByCourse;
|
||||
}
|
||||
|
||||
// When we've got all of the courses and events we can render the events in the
|
||||
// correct course event list.
|
||||
courses.forEach(function(course) {
|
||||
var courseId = course.id;
|
||||
var events = [];
|
||||
var containerSelector = '[data-region="course-events-container"][data-course-id="' + courseId + '"]';
|
||||
var courseEventsContainer = root.find(containerSelector);
|
||||
var eventListRoot = courseEventsContainer.find(EventList.rootSelector);
|
||||
var courseGroups = eventsByCourse.groupedbycourse.filter(function(group) {
|
||||
return group.courseid == courseId;
|
||||
});
|
||||
|
||||
if (courseGroups.length) {
|
||||
// Get the events for this course.
|
||||
events = courseGroups[0].events;
|
||||
}
|
||||
|
||||
// Create a preloaded page to pass to the event list because we've already
|
||||
// loaded the first page of events.
|
||||
var pageOnePreload = $.Deferred().resolve({events: events}).promise();
|
||||
// Initialise the event list pagination area for this course.
|
||||
Str.get_string('ariaeventlistpaginationnavcourses', 'block_timeline', course.fullnamedisplay)
|
||||
.then(function(string) {
|
||||
EventList.init(eventListRoot, COURSE_EVENT_LIMIT, {'1': pageOnePreload}, string);
|
||||
return string;
|
||||
})
|
||||
.catch(function() {
|
||||
// An error is ok, just render with the default string.
|
||||
EventList.init(eventListRoot, COURSE_EVENT_LIMIT, {'1': pageOnePreload});
|
||||
});
|
||||
});
|
||||
|
||||
return eventsByCourse;
|
||||
});
|
||||
}).catch(Notification.exception);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reload the events for all of the visible courses. These events will be loaded
|
||||
* in a single request to the server.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @return {object} jQuery promise resolved with courses and events.
|
||||
*/
|
||||
var reloadCourseEvents = function(root) {
|
||||
var startReloadTime = Date.now();
|
||||
var startTime = getStartTime(root);
|
||||
var endTime = getEndTime(root);
|
||||
var courseEventsContainers = root.find(SELECTORS.COURSE_EVENTS_CONTAINER);
|
||||
var courseIds = courseEventsContainers.map(function() {
|
||||
return $(this).attr('data-course-id');
|
||||
}).get();
|
||||
|
||||
// Record when we started our request.
|
||||
setEventReloadTime(root, startReloadTime);
|
||||
|
||||
// Load all of the events for the given courses.
|
||||
return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime)
|
||||
.then(function(eventsByCourse) {
|
||||
if (hasReloadedEventsSince(root, startReloadTime)) {
|
||||
// A new reload has begun so ignore our results.
|
||||
return eventsByCourse;
|
||||
}
|
||||
|
||||
courseEventsContainers.each(function(index, container) {
|
||||
container = $(container);
|
||||
var courseId = container.attr('data-course-id');
|
||||
var courseName = container.find(SELECTORS.COURSE_NAME).text();
|
||||
var eventListContainer = container.find(EventList.rootSelector);
|
||||
var pageDeferred = $.Deferred();
|
||||
var events = [];
|
||||
var courseGroups = eventsByCourse.groupedbycourse.filter(function(group) {
|
||||
return group.courseid == courseId;
|
||||
});
|
||||
|
||||
if (courseGroups.length) {
|
||||
// Get the events just for this course.
|
||||
events = courseGroups[0].events;
|
||||
}
|
||||
|
||||
pageDeferred.resolve({events: events});
|
||||
|
||||
// Re-initialise the events list with the preloaded events we just got from
|
||||
// the server.
|
||||
Str.get_string('ariaeventlistpaginationnavcourses', 'block_timeline', courseName)
|
||||
.then(function(string) {
|
||||
EventList.init(eventListContainer, COURSE_EVENT_LIMIT, {'1': pageDeferred.promise()}, string);
|
||||
return string;
|
||||
})
|
||||
.catch(function() {
|
||||
// Ignore a failure to load the string. Just render with the default string.
|
||||
EventList.init(eventListContainer, COURSE_EVENT_LIMIT, {'1': pageDeferred.promise()});
|
||||
});
|
||||
});
|
||||
|
||||
return eventsByCourse;
|
||||
}).catch(Notification.exception);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add event listeners to load more courses for the courses view.
|
||||
*
|
||||
* @param {object} root The root element for the timeline courses view.
|
||||
*/
|
||||
var registerEventListeners = function(root) {
|
||||
CustomEvents.define(root, [CustomEvents.events.activate]);
|
||||
// Show more courses and load their events when the user clicks the "more courses"
|
||||
// button.
|
||||
root.on(CustomEvents.events.activate, SELECTORS.MORE_COURSES_BUTTON, function(e, data) {
|
||||
enableMoreCoursesButtonLoading(root);
|
||||
loadMoreCourses(root)
|
||||
.then(function() {
|
||||
disableMoreCoursesButtonLoading(root);
|
||||
return;
|
||||
})
|
||||
.catch(function() {
|
||||
disableMoreCoursesButtonLoading(root);
|
||||
});
|
||||
|
||||
if (data) {
|
||||
data.originalEvent.preventDefault();
|
||||
data.originalEvent.stopPropagation();
|
||||
}
|
||||
e.stopPropagation();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialise the timeline courses view. Begin loading the events
|
||||
* if this view is active. Add the relevant event listeners.
|
||||
*
|
||||
* This function should only be called once per page load because it
|
||||
* is adding event listeners to the page.
|
||||
*
|
||||
* @param {object} root The root element for the timeline courses view.
|
||||
*/
|
||||
var init = function(root) {
|
||||
root = $(root);
|
||||
|
||||
setEventReloadTime(root, Date.now());
|
||||
|
||||
if (root.hasClass('active')) {
|
||||
// Only load if this is active otherwise it will be lazy loaded later.
|
||||
loadMoreCourses(root);
|
||||
root.attr('data-seen', true);
|
||||
}
|
||||
|
||||
registerEventListeners(root);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the element back to it's initial state. Begin loading the events again
|
||||
* if this view is active.
|
||||
*
|
||||
* @param {object} root The root element for the timeline courses view.
|
||||
*/
|
||||
var reset = function(root) {
|
||||
root.removeAttr('data-seen');
|
||||
if (root.hasClass('active')) {
|
||||
shown(root);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* If this is the first time this view has been displayed then begin loading
|
||||
* the events.
|
||||
*
|
||||
* @param {object} root The root element for the timeline courses view.
|
||||
*/
|
||||
var shown = function(root) {
|
||||
if (!root.attr('data-seen')) {
|
||||
if (hasLoadedCourses(root)) {
|
||||
// This isn't the first time this view is shown so just reload the
|
||||
// events for the courses we've already loaded.
|
||||
reloadCourseEvents(root);
|
||||
} else {
|
||||
// We haven't loaded any courses yet so do that now.
|
||||
loadMoreCourses(root);
|
||||
}
|
||||
|
||||
root.attr('data-seen', true);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
init: init,
|
||||
reset: reset,
|
||||
shown: shown
|
||||
};
|
||||
});
|
103
blocks/timeline/amd/src/view_dates.js
Normal file
103
blocks/timeline/amd/src/view_dates.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Manage the timeline dates view for the timeline block.
|
||||
*
|
||||
* @package block_timeline
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'core/str',
|
||||
'block_timeline/event_list'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
Str,
|
||||
EventList
|
||||
) {
|
||||
|
||||
var SELECTORS = {
|
||||
EVENT_LIST_CONTAINER: '[data-region="event-list-container"]',
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialise the event list and being loading the events.
|
||||
*
|
||||
* @param {object} root The root element for the timeline dates view.
|
||||
*/
|
||||
var load = function(root) {
|
||||
var eventListContainer = root.find(SELECTORS.EVENT_LIST_CONTAINER);
|
||||
Str.get_string('ariaeventlistpaginationnavdates', 'block_timeline')
|
||||
.then(function(string) {
|
||||
EventList.init(eventListContainer, [5, 10, 25], {}, string);
|
||||
return string;
|
||||
})
|
||||
.catch(function() {
|
||||
// Ignore if we can't load the string. Still init the event list.
|
||||
EventList.init(eventListContainer, [5, 10, 25]);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialise the timeline dates view. Begin loading the events
|
||||
* if this view is active.
|
||||
*
|
||||
* @param {object} root The root element for the timeline courses view.
|
||||
*/
|
||||
var init = function(root) {
|
||||
root = $(root);
|
||||
if (root.hasClass('active')) {
|
||||
load(root);
|
||||
root.attr('data-seen', true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the view back to it's initial state. If this view is active then
|
||||
* beging loading the events.
|
||||
*
|
||||
* @param {object} root The root element for the timeline courses view.
|
||||
*/
|
||||
var reset = function(root) {
|
||||
root.removeAttr('data-seen');
|
||||
if (root.hasClass('active')) {
|
||||
load(root);
|
||||
root.attr('data-seen', true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Load the events if this is the first time the view is displayed.
|
||||
*
|
||||
* @param {object} root The root element for the timeline courses view.
|
||||
*/
|
||||
var shown = function(root) {
|
||||
if (!root.attr('data-seen')) {
|
||||
load(root);
|
||||
root.attr('data-seen', true);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
init: init,
|
||||
reset: reset,
|
||||
shown: shown
|
||||
};
|
||||
});
|
120
blocks/timeline/amd/src/view_nav.js
Normal file
120
blocks/timeline/amd/src/view_nav.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Manage the timeline view navigation for the timeline block.
|
||||
*
|
||||
* @package block_timeline
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'core/custom_interaction_events',
|
||||
'block_timeline/view'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
CustomEvents,
|
||||
View
|
||||
) {
|
||||
|
||||
var SELECTORS = {
|
||||
TIMELINE_DAY_FILTER: '[data-region="day-filter"]',
|
||||
TIMELINE_DAY_FILTER_OPTION: '[data-from]',
|
||||
TIMELINE_VIEW_SELECTOR: '[data-region="view-selector"]',
|
||||
DATA_DAYS_OFFSET: '[data-days-offset]',
|
||||
DATA_DAYS_LIMIT: '[data-days-limit]',
|
||||
};
|
||||
|
||||
/**
|
||||
* Event listener for the day selector ("Next 7 days", "Next 30 days", etc).
|
||||
*
|
||||
* @param {object} root The root element for the timeline block
|
||||
* @param {object} timelineViewRoot The root element for the timeline view
|
||||
*/
|
||||
var registerTimelineDaySelector = function(root, timelineViewRoot) {
|
||||
var timelineDaySelectorContainer = root.find(SELECTORS.TIMELINE_DAY_FILTER);
|
||||
|
||||
CustomEvents.define(timelineDaySelectorContainer, [CustomEvents.events.activate]);
|
||||
timelineDaySelectorContainer.on(
|
||||
CustomEvents.events.activate,
|
||||
SELECTORS.TIMELINE_DAY_FILTER_OPTION,
|
||||
function(e, data) {
|
||||
var option = $(e.target).closest(SELECTORS.TIMELINE_DAY_FILTER_OPTION);
|
||||
|
||||
if (option.hasClass('active')) {
|
||||
// If it's already active then we don't need to do anything.
|
||||
return;
|
||||
}
|
||||
|
||||
var daysOffset = option.attr('data-from');
|
||||
var daysLimit = option.attr('data-to');
|
||||
var elementsWithDaysOffset = root.find(SELECTORS.DATA_DAYS_OFFSET);
|
||||
|
||||
elementsWithDaysOffset.attr('data-days-offset', daysOffset);
|
||||
|
||||
if (daysLimit != undefined) {
|
||||
elementsWithDaysOffset.attr('data-days-limit', daysLimit);
|
||||
} else {
|
||||
elementsWithDaysOffset.removeAttr('data-days-limit');
|
||||
}
|
||||
|
||||
// Reset the views to reinitialise the event lists now that we've
|
||||
// updated the day limits.
|
||||
View.reset(timelineViewRoot);
|
||||
|
||||
data.originalEvent.preventDefault();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Event listener for the "sort" button in the timeline navigation that allows for
|
||||
* changing between the timeline dates and courses views.
|
||||
*
|
||||
* On a view change we tell the timeline view module that the view has been shown
|
||||
* so that it can handle how to display the appropriate view.
|
||||
*
|
||||
* @param {object} root The root element for the timeline block
|
||||
* @param {object} timelineViewRoot The root element for the timeline view
|
||||
*/
|
||||
var registerViewSelector = function(root, timelineViewRoot) {
|
||||
// Listen for when the user changes tab so that we can show the first set of courses
|
||||
// and load their events when they request the sort by courses view for the first time.
|
||||
root.find(SELECTORS.TIMELINE_VIEW_SELECTOR).on('shown shown.bs.tab', function() {
|
||||
View.shown(timelineViewRoot);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialise the timeline view navigation by adding event listeners to
|
||||
* the navigation elements.
|
||||
*
|
||||
* @param {object} root The root element for the timeline block
|
||||
* @param {object} timelineViewRoot The root element for the timeline view
|
||||
*/
|
||||
var init = function(root, timelineViewRoot) {
|
||||
root = $(root);
|
||||
registerTimelineDaySelector(root, timelineViewRoot);
|
||||
registerViewSelector(root, timelineViewRoot);
|
||||
};
|
||||
|
||||
return {
|
||||
init: init
|
||||
};
|
||||
});
|
72
blocks/timeline/block_timeline.php
Normal file
72
blocks/timeline/block_timeline.php
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Contains the class for the Timeline block.
|
||||
*
|
||||
* @package block_timeline
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Timeline block class.
|
||||
*
|
||||
* @package block_timeline
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class block_timeline extends block_base {
|
||||
|
||||
/**
|
||||
* Init.
|
||||
*/
|
||||
public function init() {
|
||||
$this->title = get_string('pluginname', 'block_timeline');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contents.
|
||||
*
|
||||
* @return stdClass contents of block
|
||||
*/
|
||||
public function get_content() {
|
||||
if (isset($this->content)) {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
$renderable = new \block_timeline\output\main();
|
||||
$renderer = $this->page->get_renderer('block_timeline');
|
||||
|
||||
$this->content = (object) [
|
||||
'text' => $renderer->render($renderable),
|
||||
'footer' => ''
|
||||
];
|
||||
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locations where block can be displayed.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function applicable_formats() {
|
||||
return array('my' => true);
|
||||
}
|
||||
}
|
81
blocks/timeline/classes/output/main.php
Normal file
81
blocks/timeline/classes/output/main.php
Normal file
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Class containing data for timeline block.
|
||||
*
|
||||
* @package block_timeline
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace block_timeline\output;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use renderable;
|
||||
use renderer_base;
|
||||
use templatable;
|
||||
use core_course\external\course_summary_exporter;
|
||||
|
||||
require_once($CFG->dirroot . '/course/lib.php');
|
||||
require_once($CFG->libdir . '/completionlib.php');
|
||||
|
||||
/**
|
||||
* Class containing data for timeline block.
|
||||
*
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class main implements renderable, templatable {
|
||||
|
||||
/** Number of courses to load per page */
|
||||
const COURSES_PER_PAGE = 2;
|
||||
|
||||
/**
|
||||
* Export this data so it can be used as the context for a mustache template.
|
||||
*
|
||||
* @param \renderer_base $output
|
||||
* @return stdClass
|
||||
*/
|
||||
public function export_for_template(renderer_base $output) {
|
||||
|
||||
$nocoursesurl = $output->image_url('courses', 'block_timeline')->out();
|
||||
$noeventsurl = $output->image_url('activities', 'block_timeline')->out();
|
||||
|
||||
$requiredproperties = course_summary_exporter::define_properties();
|
||||
$fields = join(',', array_keys($requiredproperties));
|
||||
$courses = course_get_enrolled_courses_for_logged_in_user(0, 0, null, $fields);
|
||||
list($inprogresscourses, $processedcount) = course_filter_courses_by_timeline_classification(
|
||||
$courses,
|
||||
COURSE_TIMELINE_INPROGRESS,
|
||||
self::COURSES_PER_PAGE
|
||||
);
|
||||
$formattedcourses = array_map(function($course) use ($output) {
|
||||
\context_helper::preload_from_record($course);
|
||||
$context = \context_course::instance($course->id);
|
||||
$exporter = new course_summary_exporter($course, ['context' => $context]);
|
||||
return $exporter->export($output);
|
||||
}, $inprogresscourses);
|
||||
|
||||
return [
|
||||
'midnight' => usergetmidnight(time()),
|
||||
'coursepages' => [$formattedcourses],
|
||||
'urls' => [
|
||||
'nocourses' => $nocoursesurl,
|
||||
'noevents' => $noeventsurl
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
48
blocks/timeline/classes/output/renderer.php
Normal file
48
blocks/timeline/classes/output/renderer.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Timeline block rendrer.
|
||||
*
|
||||
* @package block_timeline
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
namespace block_timeline\output;
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
use plugin_renderer_base;
|
||||
use renderable;
|
||||
|
||||
/**
|
||||
* Timeline block renderer.
|
||||
*
|
||||
* @package block_timeline
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class renderer extends plugin_renderer_base {
|
||||
|
||||
/**
|
||||
* Return the main content for the block timeline.
|
||||
*
|
||||
* @param main $main The main renderable
|
||||
* @return string HTML string
|
||||
*/
|
||||
public function render_main(main $main) {
|
||||
return $this->render_from_template('block_timeline/main', $main->export_for_template($this));
|
||||
}
|
||||
}
|
|
@ -15,38 +15,32 @@
|
|||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Contains functions called by core.
|
||||
* Privacy Subsystem implementation for block_timeline.
|
||||
*
|
||||
* @package block_myoverview
|
||||
* @copyright 2017 Mark Nelson <markn@moodle.com>
|
||||
* @package block_timeline
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace block_timeline\privacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* The timeline view.
|
||||
*/
|
||||
define('BLOCK_MYOVERVIEW_TIMELINE_VIEW', 'timeline');
|
||||
|
||||
/**
|
||||
* The courses view.
|
||||
*/
|
||||
define('BLOCK_MYOVERVIEW_COURSES_VIEW', 'courses');
|
||||
|
||||
/**
|
||||
* Returns the name of the user preferences as well as the details this plugin uses.
|
||||
* Privacy Subsystem for block_timeline.
|
||||
*
|
||||
* @return array
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
function block_myoverview_user_preferences() {
|
||||
$preferences = array();
|
||||
$preferences['block_myoverview_last_tab'] = array(
|
||||
'type' => PARAM_ALPHA,
|
||||
'null' => NULL_NOT_ALLOWED,
|
||||
'default' => BLOCK_MYOVERVIEW_TIMELINE_VIEW,
|
||||
'choices' => array(BLOCK_MYOVERVIEW_TIMELINE_VIEW, BLOCK_MYOVERVIEW_COURSES_VIEW)
|
||||
);
|
||||
class provider implements \core_privacy\local\metadata\null_provider {
|
||||
|
||||
return $preferences;
|
||||
/**
|
||||
* 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';
|
||||
}
|
||||
}
|
50
blocks/timeline/db/access.php
Normal file
50
blocks/timeline/db/access.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Capabilities for the timeline block.
|
||||
*
|
||||
* @package block_timeline
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$capabilities = array(
|
||||
|
||||
'block/timeline:myaddinstance' => array(
|
||||
'captype' => 'write',
|
||||
'contextlevel' => CONTEXT_SYSTEM,
|
||||
'archetypes' => array(
|
||||
'user' => CAP_ALLOW
|
||||
),
|
||||
|
||||
'clonepermissionsfrom' => 'moodle/my:manageblocks'
|
||||
),
|
||||
|
||||
'block/timeline:addinstance' => array(
|
||||
'riskbitmask' => RISK_SPAM | RISK_XSS,
|
||||
|
||||
'captype' => 'write',
|
||||
'contextlevel' => CONTEXT_BLOCK,
|
||||
'archetypes' => array(
|
||||
'manager' => CAP_ALLOW
|
||||
),
|
||||
|
||||
'clonepermissionsfrom' => 'moodle/site:manageblocks'
|
||||
)
|
||||
);
|
108
blocks/timeline/db/install.php
Normal file
108
blocks/timeline/db/install.php
Normal file
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Timeline block installation.
|
||||
*
|
||||
* @package block_timeline
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Add the timeline block to the dashboard for all users by default
|
||||
* when it is installed.
|
||||
*/
|
||||
function xmldb_block_timeline_install() {
|
||||
global $DB;
|
||||
|
||||
if ($DB->count_records('block_instances') < 1) {
|
||||
// Only add the timeline block if it's being installed on an existing site.
|
||||
// For new sites it will be added by blocks_add_default_system_blocks().
|
||||
return;
|
||||
}
|
||||
|
||||
if ($defaultmypage = $DB->get_record('my_pages', array('userid' => null, 'name' => '__default', 'private' => 1))) {
|
||||
$subpagepattern = $defaultmypage->id;
|
||||
} else {
|
||||
$subpagepattern = null;
|
||||
}
|
||||
|
||||
$page = new moodle_page();
|
||||
$systemcontext = context_system::instance();
|
||||
$page->set_context($systemcontext);
|
||||
// Add the block to the default /my.
|
||||
$page->blocks->add_region(BLOCK_POS_RIGHT);
|
||||
$page->blocks->add_block('timeline', BLOCK_POS_RIGHT, 0, false, 'my-index', $subpagepattern);
|
||||
|
||||
// Now we need to find all users that have viewed their dashboard because it'll have
|
||||
// made duplicates of the default block_instances for them so they won't see the new
|
||||
// timeline block without the admin resetting all of the dashboards.
|
||||
//
|
||||
// Instead we'll just add the timeline block to their dashboards here. We will only
|
||||
// add the timeline block if they still have the myoverview block.
|
||||
$sql = "SELECT parentcontextid, subpagepattern
|
||||
FROM {block_instances}
|
||||
WHERE pagetypepattern = 'my-index'
|
||||
AND blockname = 'myoverview'
|
||||
AND parentcontextid != ?";
|
||||
$params = [$systemcontext->id];
|
||||
$existingrecords = $DB->get_recordset_sql($sql, $params);
|
||||
$blockinstances = [];
|
||||
$seencontexts = [];
|
||||
$now = time();
|
||||
|
||||
foreach ($existingrecords as $existingrecord) {
|
||||
$parentcontextid = $existingrecord->parentcontextid;
|
||||
if (isset($seencontexts[$parentcontextid])) {
|
||||
// If we've seen this context already then skip it because we don't want
|
||||
// to add duplicate timeline blocks to the same context. This happens
|
||||
// if something funny is going on with the subpagepattern.
|
||||
continue;
|
||||
} else {
|
||||
$seencontexts[$parentcontextid] = true;
|
||||
}
|
||||
|
||||
$blockinstances[] = [
|
||||
'blockname' => 'timeline',
|
||||
'parentcontextid' => $parentcontextid,
|
||||
'showinsubcontexts' => false,
|
||||
'pagetypepattern' => 'my-index',
|
||||
'subpagepattern' => $existingrecord->subpagepattern,
|
||||
'defaultregion' => BLOCK_POS_RIGHT,
|
||||
'defaultweight' => 0,
|
||||
'configdata' => '',
|
||||
'timecreated' => $now,
|
||||
'timemodified' => $now,
|
||||
];
|
||||
|
||||
if (count($blockinstances) >= 1000) {
|
||||
// Insert after every 1000 records so that the memory usage doesn't
|
||||
// get out of control.
|
||||
$DB->insert_records('block_instances', $blockinstances);
|
||||
$blockinstances = [];
|
||||
}
|
||||
}
|
||||
|
||||
$existingrecords->close();
|
||||
|
||||
if (!empty($blockinstances)) {
|
||||
// Insert what ever is left over.
|
||||
$DB->insert_records('block_instances', $blockinstances);
|
||||
}
|
||||
}
|
49
blocks/timeline/lang/en/block_timeline.php
Normal file
49
blocks/timeline/lang/en/block_timeline.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Lang strings for the timeline block.
|
||||
*
|
||||
* @package block_timeline
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['ariadayfilter'] = 'Filter timeline items';
|
||||
$string['ariadayfilteroption'] = '{$a} filter option';
|
||||
$string['ariaeventlistitem'] = '{$a->name} activity in {$a->course} is due on {$a->date}';
|
||||
$string['ariaeventlistpagelimit'] = 'Show {$a} activities per page';
|
||||
$string['ariaeventlistpaginationnavdates'] = 'Timeline activities pagination';
|
||||
$string['ariaeventlistpaginationnavcourses'] = 'Timeline activities for course {$a} pagination';
|
||||
$string['ariaviewselector'] = 'Sort timeline items';
|
||||
$string['ariaviewselectoroption'] = '{$a} sort option';
|
||||
$string['duedate'] = 'Due date';
|
||||
$string['morecourses'] = 'More courses';
|
||||
$string['timeline:addinstance'] = 'Add a new timeline block';
|
||||
$string['timeline:myaddinstance'] = 'Add a new timeline block to Dashboard';
|
||||
$string['nocoursesinprogress'] = 'No in progress courses';
|
||||
$string['noevents'] = 'No upcoming activities due';
|
||||
$string['next30days'] = 'Next 30 days';
|
||||
$string['next7days'] = 'Next 7 days';
|
||||
$string['next3months'] = 'Next 3 months';
|
||||
$string['next6months'] = 'Next 6 months';
|
||||
$string['overdue'] = 'Overdue';
|
||||
$string['pluginname'] = 'Timeline';
|
||||
$string['sortbycourses'] = 'Sort by courses';
|
||||
$string['sortbydates'] = 'Sort by dates';
|
||||
$string['timeline'] = 'Timeline';
|
||||
$string['viewcourse'] = 'View course';
|
||||
$string['privacy:metadata'] = 'The timeline block does not store any personal data.';
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
52
blocks/timeline/pix/courses.svg
Normal file
52
blocks/timeline/pix/courses.svg
Normal file
|
@ -0,0 +1,52 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="157 -1305 148 125" preserveAspectRatio="xMinYMid meet">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
clip-path: url(#clip-Courses);
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #eee;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #c4c8cc;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
<clipPath id="clip-Courses">
|
||||
<rect x="157" y="-1305" width="148" height="125"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="Courses" class="cls-1">
|
||||
<g id="Group_44" data-name="Group 44" transform="translate(-268 -1781)">
|
||||
<ellipse id="Ellipse_41" data-name="Ellipse 41" class="cls-2" cx="74" cy="14.785" rx="74" ry="14.785" transform="translate(425 571.43)"/>
|
||||
<rect id="Rectangle_87" data-name="Rectangle 87" class="cls-3" width="95.097" height="110.215" transform="translate(451.909 476)"/>
|
||||
<g id="Group_43" data-name="Group 43" transform="translate(464.04 494)">
|
||||
<rect id="Rectangle_88" data-name="Rectangle 88" class="cls-4" width="31.043" height="34" transform="translate(0)"/>
|
||||
<rect id="Rectangle_89" data-name="Rectangle 89" class="cls-4" width="31.043" height="34" transform="translate(0 42)"/>
|
||||
<rect id="Rectangle_90" data-name="Rectangle 90" class="cls-4" width="31.067" height="34" transform="translate(39.005)"/>
|
||||
<rect id="Rectangle_91" data-name="Rectangle 91" class="cls-4" width="31.067" height="34" transform="translate(39.005 42)"/>
|
||||
<rect id="Rectangle_92" data-name="Rectangle 92" class="cls-3" width="23.023" height="3.18" transform="translate(3.081 16.549)"/>
|
||||
<rect id="Rectangle_93" data-name="Rectangle 93" class="cls-3" width="23.023" height="3.18" transform="translate(3.081 58.549)"/>
|
||||
<rect id="Rectangle_94" data-name="Rectangle 94" class="cls-3" width="23.023" height="3.18" transform="translate(43.122 16.549)"/>
|
||||
<rect id="Rectangle_95" data-name="Rectangle 95" class="cls-3" width="23.023" height="3.18" transform="translate(43.122 58.549)"/>
|
||||
<rect id="Rectangle_96" data-name="Rectangle 96" class="cls-3" width="14.014" height="3.18" transform="translate(3.081 21.825)"/>
|
||||
<rect id="Rectangle_97" data-name="Rectangle 97" class="cls-3" width="18.845" height="3.18" transform="translate(3.081 26.825)"/>
|
||||
<rect id="Rectangle_98" data-name="Rectangle 98" class="cls-3" width="14.014" height="3.18" transform="translate(3.081 63.825)"/>
|
||||
<rect id="Rectangle_99" data-name="Rectangle 99" class="cls-3" width="18.845" height="3.18" transform="translate(3.081 68.825)"/>
|
||||
<rect id="Rectangle_100" data-name="Rectangle 100" class="cls-3" width="14.014" height="3.18" transform="translate(43.122 21.825)"/>
|
||||
<rect id="Rectangle_101" data-name="Rectangle 101" class="cls-3" width="18.845" height="3.18" transform="translate(43.122 26.825)"/>
|
||||
<rect id="Rectangle_102" data-name="Rectangle 102" class="cls-3" width="14.014" height="3.18" transform="translate(43.122 63.825)"/>
|
||||
<rect id="Rectangle_103" data-name="Rectangle 103" class="cls-3" width="18.845" height="3.18" transform="translate(43.122 68.825)"/>
|
||||
<ellipse id="Ellipse_42" data-name="Ellipse 42" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(3.003 3.55)"/>
|
||||
<ellipse id="Ellipse_43" data-name="Ellipse 43" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(3.003 45.55)"/>
|
||||
<ellipse id="Ellipse_44" data-name="Ellipse 44" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(43.044 3.55)"/>
|
||||
<ellipse id="Ellipse_45" data-name="Ellipse 45" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(43.044 45.55)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
|
@ -0,0 +1,40 @@
|
|||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_timeline/course-item-loading-placeholder
|
||||
|
||||
This template renders the each course block containing a summary and calendar events.
|
||||
|
||||
Example context (json):
|
||||
{}
|
||||
}}
|
||||
<li class="list-group-item mt-3 p-0 border-0">
|
||||
<div class="w-50 bg-pulse-grey mb-2" style="height: 20px"></div>
|
||||
<div>
|
||||
<ul class="pl-0 list-group list-group-flush">
|
||||
{{> block_timeline/placeholder-event-list-item }}
|
||||
{{> block_timeline/placeholder-event-list-item }}
|
||||
{{> block_timeline/placeholder-event-list-item }}
|
||||
{{> block_timeline/placeholder-event-list-item }}
|
||||
{{> block_timeline/placeholder-event-list-item }}
|
||||
</ul>
|
||||
<div class="pt-3 d-flex justify-content-between">
|
||||
<div class="w-25 bg-pulse-grey" style="height: 35px"></div>
|
||||
<div class="w-25 bg-pulse-grey" style="height: 35px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
|
@ -15,7 +15,7 @@
|
|||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_myoverview/course-item
|
||||
@template block_timeline/course-item
|
||||
|
||||
This template renders the each course block containing a summary and calendar events.
|
||||
|
||||
|
@ -26,19 +26,11 @@
|
|||
"summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
|
||||
}
|
||||
}}
|
||||
<li class="list-group-item m-y-1">
|
||||
<div data-region="course-events-container" id="course-events-container-{{id}}" data-course-id="{{id}}">
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
{{> block_myoverview/course-summary }}
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
{{< block_myoverview/course-event-list }}
|
||||
{{$limit}}10{{/limit}}
|
||||
{{$offset}}0{{/offset}}
|
||||
{{$courseid}}{{id}}{{/courseid}}
|
||||
{{/ block_myoverview/course-event-list }}
|
||||
</div>
|
||||
<li class="list-group-item mt-3 p-0 border-0">
|
||||
<div data-region="course-events-container" id="course-events-container-{{id}}" data-course-id="{{id}}">
|
||||
<h4 class="h5"><a href="{{viewurl}}" data-region="course-name">{{{fullnamedisplay}}}</a></h4>
|
||||
{{< block_timeline/event-list }}
|
||||
{{$courseid}}{{id}}{{/courseid}}
|
||||
{{/ block_timeline/event-list }}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
|
@ -15,7 +15,7 @@
|
|||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_myoverview/course-item
|
||||
@template block_timeline/course-items
|
||||
|
||||
This template renders the each course block containing a summary and calendar events.
|
||||
|
||||
|
@ -26,19 +26,6 @@
|
|||
"summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
|
||||
}
|
||||
}}
|
||||
<li class="list-group-item well well-small">
|
||||
<div data-region="course-events-container" id="course-events-container-{{id}}" data-course-id="{{id}}">
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
{{> block_myoverview/course-summary }}
|
||||
</div>
|
||||
<div class="span9">
|
||||
{{< block_myoverview/course-event-list }}
|
||||
{{$limit}}10{{/limit}}
|
||||
{{$offset}}0{{/offset}}
|
||||
{{$courseid}}{{id}}{{/courseid}}
|
||||
{{/ block_myoverview/course-event-list }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{#courses}}
|
||||
{{> block_timeline/course-item }}
|
||||
{{/courses}}
|
|
@ -15,24 +15,25 @@
|
|||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_myoverview/event-list-group
|
||||
@template block_timeline/event-list-content
|
||||
|
||||
This template renders a list of events for the myoverview block.
|
||||
This template renders a group of event list items for the timeline block.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"enddate": "Nov 4th, 10am",
|
||||
"name": "Assignment due 1",
|
||||
"url": "https://www.google.com",
|
||||
"timesort": 1490320388,
|
||||
"course": {
|
||||
"fullname": "Course 1"
|
||||
"fullnamedisplay": "Course 1"
|
||||
},
|
||||
"action": {
|
||||
"name": "Submit assignment",
|
||||
"url": "https://www.google.com",
|
||||
"itemcount": 1
|
||||
"itemcount": 1,
|
||||
"actionable": true
|
||||
},
|
||||
"icon": {
|
||||
"key": "icon",
|
||||
|
@ -41,16 +42,17 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"enddate": "Nov 4th, 10am",
|
||||
"name": "Assignment due 2",
|
||||
"url": "https://www.google.com",
|
||||
"timesort": 1490320388,
|
||||
"course": {
|
||||
"fullname": "Course 1"
|
||||
"fullnamedisplay": "Course 1"
|
||||
},
|
||||
"action": {
|
||||
"name": "Submit assignment",
|
||||
"url": "https://www.google.com",
|
||||
"itemcount": 1
|
||||
"itemcount": 1,
|
||||
"actionable": true
|
||||
},
|
||||
"icon": {
|
||||
"key": "icon",
|
||||
|
@ -61,15 +63,9 @@
|
|||
]
|
||||
}
|
||||
}}
|
||||
<div data-region="event-list-group-container"
|
||||
data-start-day="{{$startday}}0{{/startday}}"
|
||||
data-end-day="{{$endday}}{{/endday}}"
|
||||
class="hidden">
|
||||
|
||||
<h5 class="{{$extratitleclasses}}{{/extratitleclasses}}" id="event-list-title-{{uniqid}}"><strong>{{$title}}{{/title}}</strong></h5>
|
||||
<ul class="unstyled well well-small" data-region="event-list" aria-describedby="event-list-title-{{uniqid}}">
|
||||
{{$eventlistitems}}
|
||||
{{> block_myoverview/event-list-items }}
|
||||
{{/eventlistitems}}
|
||||
</ul>
|
||||
<div class="border-bottom pb-2">
|
||||
{{#eventsbyday}}
|
||||
<h5 class="h6 mt-3 mb-0 {{#past}}text-danger{{/past}}">{{#userdate}} {{dayTimestamp}}, {{#str}} strftimedayshort, core_langconfig {{/str}} {{/userdate}}</h5>
|
||||
{{> block_timeline/event-list-items }}
|
||||
{{/eventsbyday}}
|
||||
</div>
|
63
blocks/timeline/templates/event-list-item.mustache
Normal file
63
blocks/timeline/templates/event-list-item.mustache
Normal file
|
@ -0,0 +1,63 @@
|
|||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_timeline/event-list-item
|
||||
|
||||
This template renders an event list item for the timeline block.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"name": "Assignment due 1",
|
||||
"url": "https://www.google.com",
|
||||
"timesort": 1490320388,
|
||||
"course": {
|
||||
"fullnamedisplay": "Course 1"
|
||||
},
|
||||
"action": {
|
||||
"name": "Submit assignment",
|
||||
"url": "https://www.google.com",
|
||||
"itemcount": 1,
|
||||
"showitemcount": true,
|
||||
"actionable": true
|
||||
},
|
||||
"icon": {
|
||||
"key": "icon",
|
||||
"component": "mod_assign",
|
||||
"alttext": "Assignment icon"
|
||||
}
|
||||
}
|
||||
}}
|
||||
<a
|
||||
class="list-group-item list-group-item-action flex-column py-2 pl-0 pr-0 border-0"
|
||||
href="{{{action.url}}}"
|
||||
title="{{name}}"
|
||||
data-region="event-list-item"
|
||||
aria-label='{{#str}} ariaeventlistitem, block_timeline, { "name": "{{name}}", "course": "{{course.fullnamedisplay}}", "date": "{{#userdate}} {{timesort}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}" } {{/str}}'
|
||||
>
|
||||
<div class="d-flex">
|
||||
<div class="icon-size-4 d-flex align-self-center">
|
||||
{{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}}
|
||||
</div>
|
||||
<div class="w-100 event-name-container text-truncate line-height-3">
|
||||
<h6 class="event-name text-truncate mb-0">{{{name}}}</h6>
|
||||
<small class="text-muted text-truncate m-b-0">{{{course.fullnamedisplay}}}</small>
|
||||
</div>
|
||||
<small class="text-right text-nowrap ml-1">
|
||||
{{#userdate}} {{timesort}}, {{#str}} strftimetime24, core_langconfig {{/str}} {{/userdate}}
|
||||
</small>
|
||||
</div>
|
||||
</a>
|
|
@ -15,10 +15,9 @@
|
|||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_myoverview/course-event-list-items
|
||||
@template block_timeline/event-list-items
|
||||
|
||||
This template renders a group of event list items for the myoverview block
|
||||
sort by courses view.
|
||||
This template renders a group of event list items for the timeline block.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
|
@ -27,6 +26,9 @@
|
|||
"name": "Assignment due 1",
|
||||
"url": "https://www.google.com",
|
||||
"timesort": 1490320388,
|
||||
"course": {
|
||||
"fullnamedisplay": "Course 1"
|
||||
},
|
||||
"action": {
|
||||
"name": "Submit assignment",
|
||||
"url": "https://www.google.com",
|
||||
|
@ -43,6 +45,9 @@
|
|||
"name": "Assignment due 2",
|
||||
"url": "https://www.google.com",
|
||||
"timesort": 1490320388,
|
||||
"course": {
|
||||
"fullnamedisplay": "Course 1"
|
||||
},
|
||||
"action": {
|
||||
"name": "Submit assignment",
|
||||
"url": "https://www.google.com",
|
||||
|
@ -58,6 +63,8 @@
|
|||
]
|
||||
}
|
||||
}}
|
||||
<div class="pl-0 list-group list-group-flush">
|
||||
{{#events}}
|
||||
{{> block_myoverview/course-event-list-item }}
|
||||
{{> block_timeline/event-list-item }}
|
||||
{{/events}}
|
||||
</div>
|
55
blocks/timeline/templates/event-list.mustache
Normal file
55
blocks/timeline/templates/event-list.mustache
Normal file
|
@ -0,0 +1,55 @@
|
|||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_timeline/event-list
|
||||
|
||||
This template renders a list of events for the timeline block.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
}
|
||||
}}
|
||||
<div data-region="event-list-container"
|
||||
data-days-offset="{{$daysoffset}}{{#hasdaysoffset}}{{daysoffset}}{{/hasdaysoffset}}{{^hasdaysoffset}}0{{/hasdaysoffset}}{{/daysoffset}}"
|
||||
{{^nodayslimit}}data-days-limit="{{$dayslimit}}{{#hasdayslimit}}{{dayslimit}}{{/hasdayslimit}}{{^hasdayslimit}}30{{/hasdayslimit}}{{/dayslimit}}"{{/nodayslimit}}
|
||||
data-course-id="{{$courseid}}{{/courseid}}"
|
||||
data-midnight="{{midnight}}"
|
||||
>
|
||||
<div data-region="event-list-loading-placeholder">
|
||||
<ul class="pl-0 list-group list-group-flush">
|
||||
{{> block_timeline/placeholder-event-list-item }}
|
||||
{{> block_timeline/placeholder-event-list-item }}
|
||||
{{> block_timeline/placeholder-event-list-item }}
|
||||
{{> block_timeline/placeholder-event-list-item }}
|
||||
{{> block_timeline/placeholder-event-list-item }}
|
||||
</ul>
|
||||
<div class="pt-3 d-flex justify-content-between">
|
||||
<div class="w-25 bg-pulse-grey" style="height: 35px"></div>
|
||||
<div class="w-25 bg-pulse-grey" style="height: 35px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-region="event-list-content"></div>
|
||||
<div class="hidden text-xs-center text-center mt-3" data-region="empty-message">
|
||||
<img
|
||||
src="{{urls.noevents}}"
|
||||
alt="{{#str}} noevents, block_timeline {{/str}}"
|
||||
role="presentation"
|
||||
style="height: 70px; width: 70px"
|
||||
>
|
||||
<p class="text-muted m-t-1">{{#str}} noevents, block_timeline {{/str}}</p>
|
||||
</div>
|
||||
</div>
|
54
blocks/timeline/templates/main.mustache
Normal file
54
blocks/timeline/templates/main.mustache
Normal file
|
@ -0,0 +1,54 @@
|
|||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_timeline/main
|
||||
|
||||
This template renders the main content area for the timeline block.
|
||||
|
||||
Example context (json):
|
||||
{}
|
||||
}}
|
||||
|
||||
<div id="block-timeline-{{uniqid}}" class="block-timeline" data-region="timeline">
|
||||
<div class="container p-0 pb-3 border-bottom">
|
||||
<div class="row no-gutters">
|
||||
<div class="col-sm d-flex justify-content-start">
|
||||
{{> block_timeline/nav-day-filter }}
|
||||
</div>
|
||||
<div class="col-sm d-flex justify-content-end">
|
||||
{{> block_timeline/nav-view-selector }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container p-0">
|
||||
{{> block_timeline/view }}
|
||||
</div>
|
||||
</div>
|
||||
{{#js}}
|
||||
require(
|
||||
[
|
||||
'jquery',
|
||||
'block_timeline/main',
|
||||
],
|
||||
function(
|
||||
$,
|
||||
Main
|
||||
) {
|
||||
var root = $('#block-timeline-{{uniqid}}');
|
||||
Main.init(root);
|
||||
});
|
||||
{{/js}}
|
90
blocks/timeline/templates/nav-day-filter.mustache
Normal file
90
blocks/timeline/templates/nav-day-filter.mustache
Normal file
|
@ -0,0 +1,90 @@
|
|||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_timeline/nav-day-filter
|
||||
|
||||
This template renders the day range selector for the timeline view.
|
||||
|
||||
Example context (json):
|
||||
{}
|
||||
}}
|
||||
<div data-region="day-filter" class="dropdown">
|
||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle icon-no-margin" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{#pix}} i/duration {{/pix}}
|
||||
<span class="sr-only">
|
||||
{{#str}} ariadayfilter, block_timeline {{/str}}
|
||||
<span data-active-item-text>{{#str}} next30days, block_timeline {{/str}}</span>
|
||||
</span>
|
||||
</button>
|
||||
<div role="menu" class="dropdown-menu" data-show-active-item>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
data-from="-14"
|
||||
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} all, core {{/str}}{{/str}}"
|
||||
>
|
||||
{{#str}} all, core {{/str}}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
data-from="-14"
|
||||
data-to="0"
|
||||
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} overdue, block_timeline {{/str}}{{/str}}"
|
||||
>
|
||||
{{#str}} overdue, block_timeline {{/str}}
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<h6 class="dropdown-header">{{#str}} duedate, block_timeline {{/str}}</h6>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
data-from="0"
|
||||
data-to="7"
|
||||
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next7days, block_timeline {{/str}}{{/str}}"
|
||||
>
|
||||
{{#str}} next7days, block_timeline {{/str}}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item active"
|
||||
href="#"
|
||||
data-from="0"
|
||||
data-to="30"
|
||||
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next30days, block_timeline {{/str}}{{/str}}"
|
||||
>
|
||||
{{#str}} next30days, block_timeline {{/str}}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
data-from="0"
|
||||
data-to="90"
|
||||
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next3months, block_timeline {{/str}}{{/str}}"
|
||||
>
|
||||
{{#str}} next3months, block_timeline {{/str}}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
data-from="0"
|
||||
data-to="180"
|
||||
aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next6months, block_timeline {{/str}}{{/str}}"
|
||||
>
|
||||
{{#str}} next6months, block_timeline {{/str}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
51
blocks/timeline/templates/nav-view-selector.mustache
Normal file
51
blocks/timeline/templates/nav-view-selector.mustache
Normal file
|
@ -0,0 +1,51 @@
|
|||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_timeline/nav-view-selector
|
||||
|
||||
This template renders the timeline sort selector.
|
||||
|
||||
Example context (json):
|
||||
{}
|
||||
}}
|
||||
<div data-region="view-selector" class="btn-group">
|
||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle icon-no-margin" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{#pix}} i/filter {{/pix}}
|
||||
<span class="sr-only">
|
||||
{{#str}} ariaviewselector, block_timeline{{/str}}
|
||||
<span data-active-item-text>{{#str}} sortbydates, block_timeline {{/str}}</span>
|
||||
</span>
|
||||
</button>
|
||||
<div role="menu" class="dropdown-menu dropdown-menu-right list-group hidden" data-show-active-item data-skip-active-class="true">
|
||||
<a
|
||||
class="dropdown-item active"
|
||||
href="#view_dates_{{uniqid}}"
|
||||
data-toggle="tab"
|
||||
aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbydates, block_timeline {{/str}}{{/str}}"
|
||||
>
|
||||
{{#str}} sortbydates, block_timeline {{/str}}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#view_courses_{{uniqid}}"
|
||||
data-toggle="tab"
|
||||
aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbycourses, block_timeline {{/str}}{{/str}}"
|
||||
>
|
||||
{{#str}} sortbycourses, block_timeline {{/str}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,42 @@
|
|||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_timeline/event-list-item
|
||||
|
||||
This template renders an event list item loading placeholder for the timeline block.
|
||||
|
||||
Example context (json):
|
||||
{}
|
||||
}}
|
||||
<li class="list-group-item pl-0 pr-0">
|
||||
<div class="row">
|
||||
<div class="col-8 pr-0">
|
||||
<div class="d-flex flex-row align-items-center" style="height: 32px">
|
||||
<div class="bg-pulse-grey rounded-circle" style="height: 32px; width: 32px;"></div>
|
||||
<div style="flex: 1" class="pl-2">
|
||||
<div class="bg-pulse-grey w-100" style="height: 15px;"></div>
|
||||
<div class="bg-pulse-grey w-75 mt-1" style="height: 10px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4 pr-3">
|
||||
<div class="d-flex flex-row justify-content-end" style="height: 32px; padding-top: 2px">
|
||||
<div class="bg-pulse-grey w-75" style="height: 15px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
49
blocks/timeline/templates/view-courses.mustache
Normal file
49
blocks/timeline/templates/view-courses.mustache
Normal file
|
@ -0,0 +1,49 @@
|
|||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_timeline/view-courses
|
||||
|
||||
This template renders the timeline view by courses for the timeline block.
|
||||
|
||||
Example context (json):
|
||||
{}
|
||||
}}
|
||||
<div data-region="course-items-loading-placeholder">
|
||||
<ul class="list-group unstyled">
|
||||
{{> block_timeline/course-item-loading-placeholder }}
|
||||
{{> block_timeline/course-item-loading-placeholder }}
|
||||
</ul>
|
||||
<div class="bg-pulse-grey m-t-1" style="width: 100px; height: 30px; margin-left: auto; margin-right: auto"></div>
|
||||
</div>
|
||||
<ul class="list-group unstyled" data-region="courses-list"></ul>
|
||||
<div class="hidden text-xs-center text-center pt-3" data-region="more-courses-button-container">
|
||||
<button type="button" class="btn btn-secondary" data-action="more-courses">
|
||||
{{#str}} morecourses, block_timeline {{/str}}
|
||||
<span class="hidden" data-region="loading-icon-container">
|
||||
{{> core/loading }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="hidden text-xs-center text-center mt-3" data-region="no-courses-empty-message">
|
||||
<img
|
||||
src="{{urls.noevents}}"
|
||||
alt="{{#str}} nocoursesinprogress, block_timeline {{/str}}"
|
||||
role="presentation"
|
||||
style="height: 70px; width: 70px"
|
||||
>
|
||||
<p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_timeline {{/str}}</p>
|
||||
</div>
|
|
@ -15,21 +15,13 @@
|
|||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_myoverview/timeline-view-dates
|
||||
@template block_timeline/view-dates
|
||||
|
||||
This template renders the timeline view by dates for the myoverview block.
|
||||
This template renders the timeline view by dates for the timeline block.
|
||||
|
||||
Example context (json):
|
||||
{}
|
||||
}}
|
||||
<div data-region="timeline-view-dates" id="timeline-view-dates-{{uniqid}}">
|
||||
{{< block_myoverview/event-list }}
|
||||
{{$limit}}20{{/limit}}
|
||||
{{/ block_myoverview/event-list }}
|
||||
<div data-region="timeline-view-dates">
|
||||
{{> block_timeline/event-list }}
|
||||
</div>
|
||||
{{#js}}
|
||||
require(['jquery', 'block_myoverview/event_list'], function($, EventList) {
|
||||
var root = $("#timeline-view-dates-{{uniqid}}").find('[data-region="event-list-container"]');
|
||||
EventList.load(root);
|
||||
});
|
||||
{{/js}}
|
44
blocks/timeline/templates/view.mustache
Normal file
44
blocks/timeline/templates/view.mustache
Normal file
|
@ -0,0 +1,44 @@
|
|||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template block_timeline/view
|
||||
|
||||
This template renders the timeline view for the timeline block.
|
||||
|
||||
Example context (json):
|
||||
{}
|
||||
}}
|
||||
<div data-region="timeline-view">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active fade show" data-region="view-dates" id="view_dates_{{uniqid}}">
|
||||
{{> block_timeline/view-dates }}
|
||||
</div>
|
||||
<div
|
||||
class="tab-pane fade"
|
||||
data-region="view-courses"
|
||||
data-midnight="{{midnight}}"
|
||||
data-limit="2"
|
||||
data-offset="0"
|
||||
data-days-limit="30"
|
||||
data-days-offset="0"
|
||||
data-no-events-url="{{urls.noevents}}"
|
||||
id="view_courses_{{uniqid}}"
|
||||
>
|
||||
{{> block_timeline/view-courses }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
72
blocks/timeline/tests/behat/block_timeline_courses.feature
Normal file
72
blocks/timeline/tests/behat/block_timeline_courses.feature
Normal file
|
@ -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"
|
88
blocks/timeline/tests/behat/block_timeline_dates.feature
Normal file
88
blocks/timeline/tests/behat/block_timeline_dates.feature
Normal file
|
@ -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"
|
|
@ -15,25 +15,15 @@
|
|||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Settings for the overview block.
|
||||
* Version details for the timeline block.
|
||||
*
|
||||
* @package block_myoverview
|
||||
* @copyright 2017 Mark Nelson <markn@moodle.com>
|
||||
* @package block_timeline
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/blocks/myoverview/lib.php');
|
||||
|
||||
if ($ADMIN->fulltree) {
|
||||
|
||||
$options = [
|
||||
BLOCK_MYOVERVIEW_TIMELINE_VIEW => get_string('timeline', 'block_myoverview'),
|
||||
BLOCK_MYOVERVIEW_COURSES_VIEW => get_string('courses')
|
||||
];
|
||||
|
||||
$settings->add(new admin_setting_configselect('block_myoverview/defaulttab',
|
||||
get_string('defaulttab', 'block_myoverview'),
|
||||
get_string('defaulttab_desc', 'block_myoverview'), 'timeline', $options));
|
||||
}
|
||||
$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).
|
|
@ -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
|
||||
|
|
1
course/amd/build/repository.min.js
vendored
Normal file
1
course/amd/build/repository.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
define(["jquery","core/ajax"],function(a,b){var c=function(a,c,d,e){var f={classification:a};"undefined"!=typeof c&&(f.limit=c),"undefined"!=typeof d&&(f.offset=d),"undefined"!=typeof e&&(f.sort=e);var g={methodname:"core_course_get_enrolled_courses_by_timeline_classification",args:f};return b.call([g])[0]};return{getEnrolledCoursesByTimelineClassification:c}});
|
63
course/amd/src/repository.js
Normal file
63
course/amd/src/repository.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* A javascript module to handle course ajax actions.
|
||||
*
|
||||
* @module core_course/repository
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(['jquery', 'core/ajax'], function($, Ajax) {
|
||||
|
||||
/**
|
||||
* Get the list of courses that the logged in user is enrolled in for a given
|
||||
* timeline classification.
|
||||
*
|
||||
* @param {string} classification past, inprogress, or future
|
||||
* @param {int} limit Only return this many results
|
||||
* @param {int} offset Skip this many results from the start of the result set
|
||||
* @param {string} sort Column to sort by and direction, e.g. 'shortname asc'
|
||||
* @return {object} jQuery promise resolved with courses.
|
||||
*/
|
||||
var getEnrolledCoursesByTimelineClassification = function(classification, limit, offset, sort) {
|
||||
var args = {
|
||||
classification: classification
|
||||
};
|
||||
|
||||
if (typeof limit !== 'undefined') {
|
||||
args.limit = limit;
|
||||
}
|
||||
|
||||
if (typeof offset !== 'undefined') {
|
||||
args.offset = offset;
|
||||
}
|
||||
|
||||
if (typeof sort !== 'undefined') {
|
||||
args.sort = sort;
|
||||
}
|
||||
|
||||
var request = {
|
||||
methodname: 'core_course_get_enrolled_courses_by_timeline_classification',
|
||||
args: args
|
||||
};
|
||||
|
||||
return Ajax.call([request])[0];
|
||||
};
|
||||
|
||||
return {
|
||||
getEnrolledCoursesByTimelineClassification: getEnrolledCoursesByTimelineClassification
|
||||
};
|
||||
});
|
|
@ -26,6 +26,8 @@
|
|||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
use core_course\external\course_summary_exporter;
|
||||
|
||||
require_once("$CFG->libdir/externallib.php");
|
||||
|
||||
/**
|
||||
|
@ -3553,4 +3555,114 @@ class core_course_external extends external_api {
|
|||
public static function edit_section_returns() {
|
||||
return new external_value(PARAM_RAW, 'Additional data for javascript (JSON-encoded string)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns description of method parameters
|
||||
*
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function get_enrolled_courses_by_timeline_classification_parameters() {
|
||||
return new external_function_parameters(
|
||||
array(
|
||||
'classification' => new external_value(PARAM_ALPHA, 'future, inprogress, or past'),
|
||||
'limit' => new external_value(PARAM_INT, 'Result set limit', VALUE_DEFAULT, 0),
|
||||
'offset' => new external_value(PARAM_INT, 'Result set offset', VALUE_DEFAULT, 0),
|
||||
'sort' => new external_value(PARAM_TEXT, 'Sort string', VALUE_DEFAULT, null)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get courses matching the given timeline classification.
|
||||
*
|
||||
* NOTE: The offset applies to the unfiltered full set of courses before the classification
|
||||
* filtering is done.
|
||||
* E.g.
|
||||
* If the user is enrolled in 5 courses:
|
||||
* c1, c2, c3, c4, and c5
|
||||
* And c4 and c5 are 'future' courses
|
||||
*
|
||||
* If a request comes in for future courses with an offset of 1 it will mean that
|
||||
* c1 is skipped (because the offset applies *before* the classification filtering)
|
||||
* and c4 and c5 will be return.
|
||||
*
|
||||
* @param string $classification past, inprogress, or future
|
||||
* @param int $limit Result set limit
|
||||
* @param int $offset Offset the full course set before timeline classification is applied
|
||||
* @param string $sort SQL sort string for results
|
||||
* @return array list of courses and warnings
|
||||
* @throws invalid_parameter_exception
|
||||
*/
|
||||
public static function get_enrolled_courses_by_timeline_classification(
|
||||
string $classification,
|
||||
int $limit = 0,
|
||||
int $offset = 0,
|
||||
string $sort = null
|
||||
) {
|
||||
global $CFG, $PAGE, $USER;
|
||||
require_once($CFG->dirroot . '/course/lib.php');
|
||||
|
||||
$params = self::validate_parameters(self::get_enrolled_courses_by_timeline_classification_parameters(),
|
||||
array(
|
||||
'classification' => $classification,
|
||||
'limit' => $limit,
|
||||
'offset' => $offset,
|
||||
'sort' => $sort,
|
||||
)
|
||||
);
|
||||
|
||||
$classification = $params['classification'];
|
||||
$limit = $params['limit'];
|
||||
$offset = $params['offset'];
|
||||
$sort = $params['sort'];
|
||||
|
||||
switch($classification) {
|
||||
case COURSE_TIMELINE_PAST:
|
||||
break;
|
||||
case COURSE_TIMELINE_INPROGRESS:
|
||||
break;
|
||||
case COURSE_TIMELINE_FUTURE:
|
||||
break;
|
||||
default:
|
||||
throw new invalid_parameter_exception('Invalid classification');
|
||||
}
|
||||
|
||||
self::validate_context(context_user::instance($USER->id));
|
||||
|
||||
$requiredproperties = course_summary_exporter::define_properties();
|
||||
$fields = join(',', array_keys($requiredproperties));
|
||||
$courses = course_get_enrolled_courses_for_logged_in_user(0, $offset, $sort, $fields);
|
||||
list($filteredcourses, $processedcount) = course_filter_courses_by_timeline_classification(
|
||||
$courses,
|
||||
$classification,
|
||||
$limit
|
||||
);
|
||||
|
||||
$renderer = $PAGE->get_renderer('core');
|
||||
$formattedcourses = array_map(function($course) use ($renderer) {
|
||||
context_helper::preload_from_record($course);
|
||||
$context = context_course::instance($course->id);
|
||||
$exporter = new course_summary_exporter($course, ['context' => $context]);
|
||||
return $exporter->export($renderer);
|
||||
}, $filteredcourses);
|
||||
|
||||
return [
|
||||
'courses' => $formattedcourses,
|
||||
'nextoffset' => $offset + $processedcount
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns description of method result value
|
||||
*
|
||||
* @return external_description
|
||||
*/
|
||||
public static function get_enrolled_courses_by_timeline_classification_returns() {
|
||||
return new external_single_structure(
|
||||
array(
|
||||
'courses' => new external_multiple_structure(course_summary_exporter::get_read_structure(), 'Course'),
|
||||
'nextoffset' => new external_value(PARAM_INT, 'Offset for the next request')
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
121
course/lib.php
121
course/lib.php
|
@ -58,6 +58,7 @@ define('MOD_CLASS_RESOURCE', 1);
|
|||
define('COURSE_TIMELINE_PAST', 'past');
|
||||
define('COURSE_TIMELINE_INPROGRESS', 'inprogress');
|
||||
define('COURSE_TIMELINE_FUTURE', 'future');
|
||||
define('COURSE_DB_QUERY_LIMIT', 1000);
|
||||
|
||||
function make_log_url($module, $url) {
|
||||
switch ($module) {
|
||||
|
@ -4113,6 +4114,126 @@ function course_classify_start_date($course) {
|
|||
return $startdate->getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Group a list of courses into either past, future, or in progress.
|
||||
*
|
||||
* The return value will be an array indexed by the COURSE_TIMELINE_* constants
|
||||
* with each value being an array of courses in that group.
|
||||
* E.g.
|
||||
* [
|
||||
* COURSE_TIMELINE_PAST => [... list of past courses ...],
|
||||
* COURSE_TIMELINE_FUTURE => [],
|
||||
* COURSE_TIMELINE_INPROGRESS => []
|
||||
* ]
|
||||
*
|
||||
* @param array $courses List of courses to be grouped.
|
||||
* @return array
|
||||
*/
|
||||
function course_classify_courses_for_timeline(array $courses) {
|
||||
return array_reduce($courses, function($carry, $course) {
|
||||
$classification = course_classify_for_timeline($course);
|
||||
array_push($carry[$classification], $course);
|
||||
|
||||
return $carry;
|
||||
}, [
|
||||
COURSE_TIMELINE_PAST => [],
|
||||
COURSE_TIMELINE_FUTURE => [],
|
||||
COURSE_TIMELINE_INPROGRESS => []
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of enrolled courses for the current user.
|
||||
*
|
||||
* This function returns a Generator. The courses will be loaded from the database
|
||||
* in chunks rather than a single query.
|
||||
*
|
||||
* @param int $limit Restrict result set to this amount
|
||||
* @param int $offset Skip this number of records from the start of the result set
|
||||
* @param string|null $sort SQL string for sorting
|
||||
* @param string|null $fields SQL string for fields to be returned
|
||||
* @param int $dbquerylimit The number of records to load per DB request
|
||||
* @return Generator
|
||||
*/
|
||||
function course_get_enrolled_courses_for_logged_in_user(
|
||||
int $limit = 0,
|
||||
int $offset = 0,
|
||||
string $sort = null,
|
||||
string $fields = null,
|
||||
int $dbquerylimit = COURSE_DB_QUERY_LIMIT
|
||||
) : Generator {
|
||||
|
||||
$haslimit = !empty($limit);
|
||||
$recordsloaded = 0;
|
||||
$querylimit = (!$haslimit || $limit > $dbquerylimit) ? $dbquerylimit : $limit;
|
||||
|
||||
while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, [], false, $offset)) {
|
||||
yield from $courses;
|
||||
|
||||
$recordsloaded += $querylimit;
|
||||
|
||||
if (count($courses) < $querylimit) {
|
||||
break;
|
||||
}
|
||||
if ($haslimit && $recordsloaded >= $limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
$offset += $querylimit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the given $courses for any that match the given $classification up to the specified
|
||||
* $limit.
|
||||
*
|
||||
* This function will return the subset of courses that match the classification as well as the
|
||||
* number of courses it had to process to build that subset.
|
||||
*
|
||||
* It is recommended that for larger sets of courses this function is given a Generator that loads
|
||||
* the courses from the database in chunks.
|
||||
*
|
||||
* @param array|Traversable $courses List of courses to process
|
||||
* @param string $classification One of the COURSE_TIMELINE_* constants
|
||||
* @param int $limit Limit the number of results to this amount
|
||||
* @return array First value is the filtered courses, second value is the number of courses processed
|
||||
*/
|
||||
function course_filter_courses_by_timeline_classification(
|
||||
$courses,
|
||||
string $classification,
|
||||
int $limit = 0
|
||||
) : array {
|
||||
|
||||
if (!in_array($classification, [COURSE_TIMELINE_PAST, COURSE_TIMELINE_INPROGRESS, COURSE_TIMELINE_FUTURE])) {
|
||||
$message = 'Classification must be one of COURSE_TIMELINE_PAST, '
|
||||
. 'COURSE_TIMELINE_INPROGRESS or COURSE_TIMELINE_FUTURE';
|
||||
throw new moodle_exception($message);
|
||||
}
|
||||
|
||||
$filteredcourses = [];
|
||||
$numberofcoursesprocessed = 0;
|
||||
$filtermatches = 0;
|
||||
|
||||
foreach ($courses as $course) {
|
||||
$numberofcoursesprocessed++;
|
||||
|
||||
if ($classification == course_classify_for_timeline($course)) {
|
||||
$filteredcourses[] = $course;
|
||||
$filtermatches++;
|
||||
}
|
||||
|
||||
if ($limit && $filtermatches >= $limit) {
|
||||
// We've found the number of requested courses. No need to continue searching.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the number of filtered courses as well as the number of courses that were searched
|
||||
// in order to find the matching courses. This allows the calling code to do some kind of
|
||||
// pagination.
|
||||
return [$filteredcourses, $numberofcoursesprocessed];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check module updates since a given time.
|
||||
* This function checks for updates in the module config, file areas, completion, grades, comments and ratings.
|
||||
|
|
|
@ -4227,4 +4227,494 @@ class core_course_courselib_testcase extends advanced_testcase {
|
|||
assign_capability('moodle/backup:downloadfile', CAP_ALLOW, $teacherrole->id, $context);
|
||||
$this->assertFalse(can_download_from_backup_filearea('testing', $context, $user));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cases for the course_classify_courses_for_timeline test.
|
||||
*/
|
||||
public function get_course_classify_courses_for_timeline_test_cases() {
|
||||
$now = time();
|
||||
$day = 86400;
|
||||
|
||||
return [
|
||||
'no courses' => [
|
||||
'coursesdata' => [],
|
||||
'expected' => [
|
||||
COURSE_TIMELINE_PAST => [],
|
||||
COURSE_TIMELINE_FUTURE => [],
|
||||
COURSE_TIMELINE_INPROGRESS => []
|
||||
]
|
||||
],
|
||||
'only past' => [
|
||||
'coursesdata' => [
|
||||
[
|
||||
'shortname' => 'past1',
|
||||
'startdate' => $now - ($day * 2),
|
||||
'enddate' => $now - $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'past2',
|
||||
'startdate' => $now - ($day * 2),
|
||||
'enddate' => $now - $day
|
||||
]
|
||||
],
|
||||
'expected' => [
|
||||
COURSE_TIMELINE_PAST => ['past1', 'past2'],
|
||||
COURSE_TIMELINE_FUTURE => [],
|
||||
COURSE_TIMELINE_INPROGRESS => []
|
||||
]
|
||||
],
|
||||
'only in progress' => [
|
||||
'coursesdata' => [
|
||||
[
|
||||
'shortname' => 'inprogress1',
|
||||
'startdate' => $now - $day,
|
||||
'enddate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'inprogress2',
|
||||
'startdate' => $now - $day,
|
||||
'enddate' => $now + $day
|
||||
]
|
||||
],
|
||||
'expected' => [
|
||||
COURSE_TIMELINE_PAST => [],
|
||||
COURSE_TIMELINE_FUTURE => [],
|
||||
COURSE_TIMELINE_INPROGRESS => ['inprogress1', 'inprogress2']
|
||||
]
|
||||
],
|
||||
'only future' => [
|
||||
'coursesdata' => [
|
||||
[
|
||||
'shortname' => 'future1',
|
||||
'startdate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'future2',
|
||||
'startdate' => $now + $day
|
||||
]
|
||||
],
|
||||
'expected' => [
|
||||
COURSE_TIMELINE_PAST => [],
|
||||
COURSE_TIMELINE_FUTURE => ['future1', 'future2'],
|
||||
COURSE_TIMELINE_INPROGRESS => []
|
||||
]
|
||||
],
|
||||
'combination' => [
|
||||
'coursesdata' => [
|
||||
[
|
||||
'shortname' => 'past1',
|
||||
'startdate' => $now - ($day * 2),
|
||||
'enddate' => $now - $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'past2',
|
||||
'startdate' => $now - ($day * 2),
|
||||
'enddate' => $now - $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'inprogress1',
|
||||
'startdate' => $now - $day,
|
||||
'enddate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'inprogress2',
|
||||
'startdate' => $now - $day,
|
||||
'enddate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'future1',
|
||||
'startdate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'future2',
|
||||
'startdate' => $now + $day
|
||||
]
|
||||
],
|
||||
'expected' => [
|
||||
COURSE_TIMELINE_PAST => ['past1', 'past2'],
|
||||
COURSE_TIMELINE_FUTURE => ['future1', 'future2'],
|
||||
COURSE_TIMELINE_INPROGRESS => ['inprogress1', 'inprogress2']
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the course_classify_courses_for_timeline function.
|
||||
*
|
||||
* @dataProvider get_course_classify_courses_for_timeline_test_cases()
|
||||
* @param array $coursesdata Courses to create
|
||||
* @param array $expected Expected test results.
|
||||
*/
|
||||
public function test_course_classify_courses_for_timeline($coursesdata, $expected) {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator();
|
||||
|
||||
$courses = array_map(function($coursedata) use ($generator) {
|
||||
return $generator->create_course($coursedata);
|
||||
}, $coursesdata);
|
||||
|
||||
sort($expected[COURSE_TIMELINE_PAST]);
|
||||
sort($expected[COURSE_TIMELINE_FUTURE]);
|
||||
sort($expected[COURSE_TIMELINE_INPROGRESS]);
|
||||
|
||||
$results = course_classify_courses_for_timeline($courses);
|
||||
|
||||
$actualpast = array_map(function($result) {
|
||||
return $result->shortname;
|
||||
}, $results[COURSE_TIMELINE_PAST]);
|
||||
|
||||
$actualfuture = array_map(function($result) {
|
||||
return $result->shortname;
|
||||
}, $results[COURSE_TIMELINE_FUTURE]);
|
||||
|
||||
$actualinprogress = array_map(function($result) {
|
||||
return $result->shortname;
|
||||
}, $results[COURSE_TIMELINE_INPROGRESS]);
|
||||
|
||||
sort($actualpast);
|
||||
sort($actualfuture);
|
||||
sort($actualinprogress);
|
||||
|
||||
$this->assertEquals($expected[COURSE_TIMELINE_PAST], $actualpast);
|
||||
$this->assertEquals($expected[COURSE_TIMELINE_FUTURE], $actualfuture);
|
||||
$this->assertEquals($expected[COURSE_TIMELINE_INPROGRESS], $actualinprogress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cases for the course_get_enrolled_courses_for_logged_in_user tests.
|
||||
*/
|
||||
public function get_course_get_enrolled_courses_for_logged_in_user_test_cases() {
|
||||
$buildexpectedresult = function($limit, $offset) {
|
||||
$result = [];
|
||||
for ($i = $offset; $i < $offset + $limit; $i++) {
|
||||
$result[] = "testcourse{$i}";
|
||||
}
|
||||
return $result;
|
||||
};
|
||||
|
||||
return [
|
||||
'zero records' => [
|
||||
'dbquerylimit' => 3,
|
||||
'totalcourses' => 0,
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 1,
|
||||
'expectedresult' => $buildexpectedresult(0, 0)
|
||||
],
|
||||
'less than query limit' => [
|
||||
'dbquerylimit' => 3,
|
||||
'totalcourses' => 2,
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 1,
|
||||
'expectedresult' => $buildexpectedresult(2, 0)
|
||||
],
|
||||
'more than query limit' => [
|
||||
'dbquerylimit' => 3,
|
||||
'totalcourses' => 7,
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 3,
|
||||
'expectedresult' => $buildexpectedresult(7, 0)
|
||||
],
|
||||
'limit less than query limit' => [
|
||||
'dbquerylimit' => 3,
|
||||
'totalcourses' => 7,
|
||||
'limit' => 2,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 1,
|
||||
'expectedresult' => $buildexpectedresult(2, 0)
|
||||
],
|
||||
'limit less than query limit with offset' => [
|
||||
'dbquerylimit' => 3,
|
||||
'totalcourses' => 7,
|
||||
'limit' => 2,
|
||||
'offset' => 2,
|
||||
'expecteddbqueries' => 1,
|
||||
'expectedresult' => $buildexpectedresult(2, 2)
|
||||
],
|
||||
'limit less than total' => [
|
||||
'dbquerylimit' => 3,
|
||||
'totalcourses' => 9,
|
||||
'limit' => 6,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 2,
|
||||
'expectedresult' => $buildexpectedresult(6, 0)
|
||||
],
|
||||
'less results than limit' => [
|
||||
'dbquerylimit' => 4,
|
||||
'totalcourses' => 9,
|
||||
'limit' => 20,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 3,
|
||||
'expectedresult' => $buildexpectedresult(9, 0)
|
||||
],
|
||||
'less results than limit exact divisible' => [
|
||||
'dbquerylimit' => 3,
|
||||
'totalcourses' => 9,
|
||||
'limit' => 20,
|
||||
'offset' => 0,
|
||||
'expecteddbqueries' => 4,
|
||||
'expectedresult' => $buildexpectedresult(9, 0)
|
||||
],
|
||||
'less results than limit with offset' => [
|
||||
'dbquerylimit' => 3,
|
||||
'totalcourses' => 9,
|
||||
'limit' => 10,
|
||||
'offset' => 5,
|
||||
'expecteddbqueries' => 2,
|
||||
'expectedresult' => $buildexpectedresult(4, 5)
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the course_get_enrolled_courses_for_logged_in_user function.
|
||||
*
|
||||
* @dataProvider get_course_get_enrolled_courses_for_logged_in_user_test_cases()
|
||||
* @param int $dbquerylimit Number of records to load per DB request
|
||||
* @param int $totalcourses Number of courses to create
|
||||
* @param int $limit Maximum number of results to get.
|
||||
* @param int $offset Skip this number of results from the start of the result set.
|
||||
* @param int $expecteddbqueries The number of DB queries expected during the test.
|
||||
* @param array $expectedresult Expected test results.
|
||||
*/
|
||||
public function test_course_get_enrolled_courses_for_logged_in_user(
|
||||
$dbquerylimit,
|
||||
$totalcourses,
|
||||
$limit,
|
||||
$offset,
|
||||
$expecteddbqueries,
|
||||
$expectedresult
|
||||
) {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator();
|
||||
$student = $generator->create_user();
|
||||
|
||||
for ($i = 0; $i < $totalcourses; $i++) {
|
||||
$shortname = "testcourse{$i}";
|
||||
$course = $generator->create_course(['shortname' => $shortname]);
|
||||
$generator->enrol_user($student->id, $course->id, 'student');
|
||||
}
|
||||
|
||||
$this->setUser($student);
|
||||
|
||||
$initialquerycount = $DB->perf_get_queries();
|
||||
$courses = course_get_enrolled_courses_for_logged_in_user($limit, $offset, 'shortname ASC', 'shortname', $dbquerylimit);
|
||||
|
||||
// Loop over the result set to force the lazy loading to kick in so that we can check the
|
||||
// number of DB queries.
|
||||
$actualresult = array_map(function($course) {
|
||||
return $course->shortname;
|
||||
}, iterator_to_array($courses, false));
|
||||
|
||||
sort($expectedresult);
|
||||
|
||||
$this->assertEquals($expectedresult, $actualresult);
|
||||
$this->assertEquals($expecteddbqueries, $DB->perf_get_queries() - $initialquerycount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cases for the course_filter_courses_by_timeline_classification tests.
|
||||
*/
|
||||
public function get_course_filter_courses_by_timeline_classification_test_cases() {
|
||||
$now = time();
|
||||
$day = 86400;
|
||||
|
||||
$coursedata = [
|
||||
[
|
||||
'shortname' => 'apast',
|
||||
'startdate' => $now - ($day * 2),
|
||||
'enddate' => $now - $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'bpast',
|
||||
'startdate' => $now - ($day * 2),
|
||||
'enddate' => $now - $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'cpast',
|
||||
'startdate' => $now - ($day * 2),
|
||||
'enddate' => $now - $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'dpast',
|
||||
'startdate' => $now - ($day * 2),
|
||||
'enddate' => $now - $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'epast',
|
||||
'startdate' => $now - ($day * 2),
|
||||
'enddate' => $now - $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'ainprogress',
|
||||
'startdate' => $now - $day,
|
||||
'enddate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'binprogress',
|
||||
'startdate' => $now - $day,
|
||||
'enddate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'cinprogress',
|
||||
'startdate' => $now - $day,
|
||||
'enddate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'dinprogress',
|
||||
'startdate' => $now - $day,
|
||||
'enddate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'einprogress',
|
||||
'startdate' => $now - $day,
|
||||
'enddate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'afuture',
|
||||
'startdate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'bfuture',
|
||||
'startdate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'cfuture',
|
||||
'startdate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'dfuture',
|
||||
'startdate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'efuture',
|
||||
'startdate' => $now + $day
|
||||
]
|
||||
];
|
||||
|
||||
// Raw enrolled courses result set should be returned in this order:
|
||||
// afuture, ainprogress, apast, bfuture, binprogress, bpast, cfuture, cinprogress, cpast,
|
||||
// dfuture, dinprogress, dpast, efuture, einprogress, epast
|
||||
//
|
||||
// By classification the offset values for each record should be:
|
||||
// COURSE_TIMELINE_FUTURE
|
||||
// 0 (afuture), 3 (bfuture), 6 (cfuture), 9 (dfuture), 12 (efuture)
|
||||
// COURSE_TIMELINE_INPROGRESS
|
||||
// 1 (ainprogress), 4 (binprogress), 7 (cinprogress), 10 (dinprogress), 13 (einprogress)
|
||||
// COURSE_TIMELINE_PAST
|
||||
// 2 (apast), 5 (bpast), 8 (cpast), 11 (dpast), 14 (epast).
|
||||
return [
|
||||
'empty set' => [
|
||||
'coursedata' => [],
|
||||
'classification' => COURSE_TIMELINE_FUTURE,
|
||||
'limit' => 2,
|
||||
'offset' => 0,
|
||||
'expectedcourses' => [],
|
||||
'expectedprocessedcount' => 0
|
||||
],
|
||||
// COURSE_TIMELINE_FUTURE.
|
||||
'future not limit no offset' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => COURSE_TIMELINE_FUTURE,
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
|
||||
'expectedprocessedcount' => 15
|
||||
],
|
||||
'future no offset' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => COURSE_TIMELINE_FUTURE,
|
||||
'limit' => 2,
|
||||
'offset' => 0,
|
||||
'expectedcourses' => ['afuture', 'bfuture'],
|
||||
'expectedprocessedcount' => 4
|
||||
],
|
||||
'future offset' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => COURSE_TIMELINE_FUTURE,
|
||||
'limit' => 2,
|
||||
'offset' => 2,
|
||||
'expectedcourses' => ['bfuture', 'cfuture'],
|
||||
'expectedprocessedcount' => 5
|
||||
],
|
||||
'future exact limit' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => COURSE_TIMELINE_FUTURE,
|
||||
'limit' => 5,
|
||||
'offset' => 0,
|
||||
'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
|
||||
'expectedprocessedcount' => 13
|
||||
],
|
||||
'future limit less results' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => COURSE_TIMELINE_FUTURE,
|
||||
'limit' => 10,
|
||||
'offset' => 0,
|
||||
'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
|
||||
'expectedprocessedcount' => 15
|
||||
],
|
||||
'future limit less results with offset' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => COURSE_TIMELINE_FUTURE,
|
||||
'limit' => 10,
|
||||
'offset' => 5,
|
||||
'expectedcourses' => ['cfuture', 'dfuture', 'efuture'],
|
||||
'expectedprocessedcount' => 10
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the course_filter_courses_by_timeline_classification function.
|
||||
*
|
||||
* @dataProvider get_course_filter_courses_by_timeline_classification_test_cases()
|
||||
* @param array $coursedata Course test data to create.
|
||||
* @param string $classification Timeline classification.
|
||||
* @param int $limit Maximum number of results to return.
|
||||
* @param int $offset Results to skip at the start of the result set.
|
||||
* @param string[] $expectedcourses Expected courses in results.
|
||||
* @param int $expectedprocessedcount Expected number of course records to be processed.
|
||||
*/
|
||||
public function test_course_filter_courses_by_timeline_classification(
|
||||
$coursedata,
|
||||
$classification,
|
||||
$limit,
|
||||
$offset,
|
||||
$expectedcourses,
|
||||
$expectedprocessedcount
|
||||
) {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator();
|
||||
|
||||
$courses = array_map(function($coursedata) use ($generator) {
|
||||
return $generator->create_course($coursedata);
|
||||
}, $coursedata);
|
||||
|
||||
$student = $generator->create_user();
|
||||
|
||||
foreach ($courses as $course) {
|
||||
$generator->enrol_user($student->id, $course->id, 'student');
|
||||
}
|
||||
|
||||
$this->setUser($student);
|
||||
|
||||
$coursesgenerator = course_get_enrolled_courses_for_logged_in_user(0, $offset, 'shortname ASC', 'shortname');
|
||||
list($result, $processedcount) = course_filter_courses_by_timeline_classification(
|
||||
$coursesgenerator,
|
||||
$classification,
|
||||
$limit
|
||||
);
|
||||
|
||||
$actual = array_map(function($course) {
|
||||
return $course->shortname;
|
||||
}, $result);
|
||||
|
||||
$this->assertEquals($expectedcourses, $actual);
|
||||
$this->assertEquals($expectedprocessedcount, $processedcount);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2341,4 +2341,218 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
|
|||
$this->assertCount(1, $result['warnings']);
|
||||
$this->assertEquals(-2, $result['warnings'][0]['itemid']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cases for the get_enrolled_courses_by_timeline_classification test.
|
||||
*/
|
||||
public function get_get_enrolled_courses_by_timeline_classification_test_cases() {
|
||||
$now = time();
|
||||
$day = 86400;
|
||||
|
||||
$coursedata = [
|
||||
[
|
||||
'shortname' => 'apast',
|
||||
'startdate' => $now - ($day * 2),
|
||||
'enddate' => $now - $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'bpast',
|
||||
'startdate' => $now - ($day * 2),
|
||||
'enddate' => $now - $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'cpast',
|
||||
'startdate' => $now - ($day * 2),
|
||||
'enddate' => $now - $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'dpast',
|
||||
'startdate' => $now - ($day * 2),
|
||||
'enddate' => $now - $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'epast',
|
||||
'startdate' => $now - ($day * 2),
|
||||
'enddate' => $now - $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'ainprogress',
|
||||
'startdate' => $now - $day,
|
||||
'enddate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'binprogress',
|
||||
'startdate' => $now - $day,
|
||||
'enddate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'cinprogress',
|
||||
'startdate' => $now - $day,
|
||||
'enddate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'dinprogress',
|
||||
'startdate' => $now - $day,
|
||||
'enddate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'einprogress',
|
||||
'startdate' => $now - $day,
|
||||
'enddate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'afuture',
|
||||
'startdate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'bfuture',
|
||||
'startdate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'cfuture',
|
||||
'startdate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'dfuture',
|
||||
'startdate' => $now + $day
|
||||
],
|
||||
[
|
||||
'shortname' => 'efuture',
|
||||
'startdate' => $now + $day
|
||||
]
|
||||
];
|
||||
|
||||
// Raw enrolled courses result set should be returned in this order:
|
||||
// afuture, ainprogress, apast, bfuture, binprogress, bpast, cfuture, cinprogress, cpast,
|
||||
// dfuture, dinprogress, dpast, efuture, einprogress, epast
|
||||
//
|
||||
// By classification the offset values for each record should be:
|
||||
// COURSE_TIMELINE_FUTURE
|
||||
// 0 (afuture), 3 (bfuture), 6 (cfuture), 9 (dfuture), 12 (efuture)
|
||||
// COURSE_TIMELINE_INPROGRESS
|
||||
// 1 (ainprogress), 4 (binprogress), 7 (cinprogress), 10 (dinprogress), 13 (einprogress)
|
||||
// COURSE_TIMELINE_PAST
|
||||
// 2 (apast), 5 (bpast), 8 (cpast), 11 (dpast), 14 (epast).
|
||||
//
|
||||
// NOTE: The offset applies to the unfiltered full set of courses before the classification
|
||||
// filtering is done.
|
||||
// E.g. In our example if an offset of 2 is given then it would mean the first
|
||||
// two courses (afuture, ainprogress) are ignored.
|
||||
return [
|
||||
'empty set' => [
|
||||
'coursedata' => [],
|
||||
'classification' => 'future',
|
||||
'limit' => 2,
|
||||
'offset' => 0,
|
||||
'expectedcourses' => [],
|
||||
'expectednextoffset' => 0
|
||||
],
|
||||
// COURSE_TIMELINE_FUTURE.
|
||||
'future not limit no offset' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'future',
|
||||
'limit' => 0,
|
||||
'offset' => 0,
|
||||
'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
|
||||
'expectednextoffset' => 15
|
||||
],
|
||||
'future no offset' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'future',
|
||||
'limit' => 2,
|
||||
'offset' => 0,
|
||||
'expectedcourses' => ['afuture', 'bfuture'],
|
||||
'expectednextoffset' => 4
|
||||
],
|
||||
'future offset' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'future',
|
||||
'limit' => 2,
|
||||
'offset' => 2,
|
||||
'expectedcourses' => ['bfuture', 'cfuture'],
|
||||
'expectednextoffset' => 7
|
||||
],
|
||||
'future exact limit' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'future',
|
||||
'limit' => 5,
|
||||
'offset' => 0,
|
||||
'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
|
||||
'expectednextoffset' => 13
|
||||
],
|
||||
'future limit less results' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'future',
|
||||
'limit' => 10,
|
||||
'offset' => 0,
|
||||
'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
|
||||
'expectednextoffset' => 15
|
||||
],
|
||||
'future limit less results with offset' => [
|
||||
'coursedata' => $coursedata,
|
||||
'classification' => 'future',
|
||||
'limit' => 10,
|
||||
'offset' => 5,
|
||||
'expectedcourses' => ['cfuture', 'dfuture', 'efuture'],
|
||||
'expectednextoffset' => 15
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the get_enrolled_courses_by_timeline_classification function.
|
||||
*
|
||||
* @dataProvider get_get_enrolled_courses_by_timeline_classification_test_cases()
|
||||
* @param array $coursedata Courses to create
|
||||
* @param string $classification Timeline classification
|
||||
* @param int $limit Maximum number of results
|
||||
* @param int $offset Offset the unfiltered courses result set by this amount
|
||||
* @param array $expectedcourses Expected courses in result
|
||||
* @param int $expectednextoffset Expected next offset value in result
|
||||
*/
|
||||
public function test_get_enrolled_courses_by_timeline_classification(
|
||||
$coursedata,
|
||||
$classification,
|
||||
$limit,
|
||||
$offset,
|
||||
$expectedcourses,
|
||||
$expectednextoffset
|
||||
) {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator();
|
||||
|
||||
$courses = array_map(function($coursedata) use ($generator) {
|
||||
return $generator->create_course($coursedata);
|
||||
}, $coursedata);
|
||||
|
||||
$student = $generator->create_user();
|
||||
|
||||
foreach ($courses as $course) {
|
||||
$generator->enrol_user($student->id, $course->id, 'student');
|
||||
}
|
||||
|
||||
$this->setUser($student);
|
||||
|
||||
// NOTE: The offset applies to the unfiltered full set of courses before the classification
|
||||
// filtering is done.
|
||||
// E.g. In our example if an offset of 2 is given then it would mean the first
|
||||
// two courses (afuture, ainprogress) are ignored.
|
||||
$result = core_course_external::get_enrolled_courses_by_timeline_classification(
|
||||
$classification,
|
||||
$limit,
|
||||
$offset,
|
||||
'shortname ASC'
|
||||
);
|
||||
$result = external_api::clean_returnvalue(
|
||||
core_course_external::get_enrolled_courses_by_timeline_classification_returns(),
|
||||
$result
|
||||
);
|
||||
|
||||
$actual = array_map(function($course) {
|
||||
return $course['shortname'];
|
||||
}, $result['courses']);
|
||||
|
||||
$this->assertEquals($expectedcourses, $actual);
|
||||
$this->assertEquals($expectednextoffset, $result['nextoffset']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1471,6 +1471,10 @@ $string['outline'] = 'Outline';
|
|||
$string['outlinereport'] = 'Outline report';
|
||||
$string['page'] = 'Page';
|
||||
$string['pagea'] = 'Page {$a}';
|
||||
$string['pagedcontentnavigation'] = 'Pagination navigation';
|
||||
$string['pagedcontentnavigationitem'] = 'Go to page {$a}';
|
||||
$string['pagedcontentnavigationactiveitem'] = 'Current page, page {$a}';
|
||||
$string['pagedcontentpagingbaritemsperpage'] = 'Show {$a} items per page';
|
||||
$string['pageheaderconfigablock'] = 'Configuring a block in {$a->fullname}';
|
||||
$string['pagepath'] = 'Page path';
|
||||
$string['pageshouldredirect'] = 'This page should automatically redirect. If nothing is happening please use the continue link below.';
|
||||
|
|
1
lib/amd/build/page_global.min.js
vendored
Normal file
1
lib/amd/build/page_global.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
define(["jquery","core/custom_interaction_events","core/str"],function(a,b,c){var d=function(){var d=a("body");b.define(d,[b.events.activate]),d.on(b.events.activate,"[data-show-active-item]",function(b){var d=a(b.target).closest(".dropdown-item"),e=d.closest("[data-show-active-item]");if(d.hasClass("dropdown-item")&&!d.hasClass("active")){var f=e.find(".dropdown-item");f.removeClass("active"),f.removeAttr("aria-current"),e.attr("data-skip-active-class")||d.addClass("active"),d.attr("aria-current",!0);var g=d.text(),h=e.parent().find('[data-toggle="dropdown"]'),i=h.find("[data-active-item-text]");i.length?i.html(g):h.html(g);var j=e.attr("data-active-item-button-aria-label-components");if(j){var k=j.split(",");k.push(g),c.get_string(k[0].trim(),k[1].trim(),k[2].trim()).then(function(a){return h.attr("aria-label",a),a})["catch"](function(){return!1})}}})},e=function(){d()};return{init:e}});
|
1
lib/amd/build/paged_content.min.js
vendored
Normal file
1
lib/amd/build/paged_content.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
define(["jquery","core/paged_content_pages","core/paged_content_paging_bar","core/paged_content_paging_bar_limit_selector","core/paged_content_paging_dropdown"],function(a,b,c,d,e){var f=function(f,g){f=a(f);var h=f.find(b.rootSelector),i=f.find(c.rootSelector),j=f.find(e.rootSelector),k=f.find(d.rootSelector),l=f.attr("id");b.init(h,l,g),i.length&&c.init(i,l),k.length&&d.init(k,l),j.length&&e.init(j,l)};return{init:f,rootSelector:'[data-region="paged-content-container"]'}});
|
2
lib/amd/build/paged_content_events.min.js
vendored
2
lib/amd/build/paged_content_events.min.js
vendored
|
@ -1 +1 @@
|
|||
define([],function(){return{SHOW_PAGES:"core-paged-content-show-pages"}});
|
||||
define([],function(){return{SHOW_PAGES:"core-paged-content-show-pages",PAGES_SHOWN:"core-paged-content-pages-shown",ALL_ITEMS_LOADED:"core-paged-content-all-items-loaded",SET_ITEMS_PER_PAGE_LIMIT:"core-paged-content-set-items-per-page-limit"}});
|
2
lib/amd/build/paged_content_factory.min.js
vendored
2
lib/amd/build/paged_content_factory.min.js
vendored
|
@ -1 +1 @@
|
|||
define(["jquery","core/templates","core/notification","core/paged_content_pages"],function(a,b,c,d){var e={PAGED_CONTENT:"core/paged_content"},f=function(a,b){for(var c={itemsperpage:b,previous:{},next:{},pages:[]},d=1;d<=a;d++){var e={number:d,page:""+d};1===d&&(e.active=!0),c.pages.push(e)}return c},g=function(a,b,c){var d={options:[]},e=0,f=0,g=a;c.hasOwnProperty("maxPages")&&(g=c.maxPages);for(var h=1;h<=g;h++){var i=0;h<=2?(i=b,f=b):(f=2*f,i=f),e+=i;var j={itemcount:i,content:e};1===h&&(j.active=!0),d.options.push(j)}return d},h=function(a,b,c){var d={pagingbar:!1,pagingdropdown:!1,skipjs:!0};return c.hasOwnProperty("dropdown")&&c.dropdown?d.pagingdropdown=g(a,b,c):d.pagingbar=f(a,b),d},i=function(a,b){var c=1;if(a>0){var d=a%b;d?(a-=d,c=a/b+1):c=a/b}return c},j=function(f,g,j,k){"undefined"==typeof k&&(k={});var l=a.Deferred(),m=i(f,g),n=h(m,g,k);return b.render(e.PAGED_CONTENT,n).then(function(b,c){b=a(b);var e=b,f=b.find(d.rootSelector);d.init(f,e,j),l.resolve(b,c)}).fail(function(a){l.reject(a)}).fail(c.exception),l},k=function(a,b,c,d){"undefined"==typeof d&&(d={});var e=a.length;return j(e,b,function(b){var d=[];return b.forEach(function(b){var c=b.offset,f=b.limit?c+b.limit:e,g=a.slice(c,f);d.push(g)}),c(d)},d)};return{createFromAjax:j,createFromStaticList:k}});
|
||||
define(["jquery","core/templates","core/notification","core/paged_content"],function(a,b,c,d){var e={PAGED_CONTENT:"core/paged_content"},f={ITEMS_PER_PAGE_SINGLE:25,ITEMS_PER_PAGE_ARRAY:[25,50,100,0],MAX_PAGES:3},g=function(){return{pagingbar:!1,pagingdropdown:!1,skipjs:!0,ignorecontrolwhileloading:!0,controlplacementbottom:!1}},h=function(){return{showitemsperpageselector:!1,itemsperpage:35,previous:!0,next:!0,activepagenumber:1,hidecontrolonsinglepage:!0,pages:[]}},i=function(a,b){var c=1;if(a>0){var d=a%b;d?(a-=d,c=a/b+1):c=a/b}return c},j=function(b,c){null===c&&(c=f.ITEMS_PER_PAGE_SINGLE),a.isArray(c)&&(c=c[0]);var d=h();d.itemsperpage=c;for(var e=i(b,c),g=1;g<=e;g++){var j={number:g,page:""+g};1===g&&(j.active=!0),d.pages.push(j)}return d},k=function(b){if(a.isArray(b)){var c=b.map(function(a){return"number"==typeof a?{value:a,active:!1}:a}),d=c.filter(function(a){return a.active});return d.length||(c[0].active=!0),c}return b},l=function(b){null===b&&(b=f.ITEMS_PER_PAGE_ARRAY);var c=h();return c.itemsperpage=k(b),c.showitemsperpageselector=a.isArray(b),c},m=function(a,b){return a?j(a,b):l(b)},n=function(b,c){if(null===b&&(b=f.ITEMS_PER_PAGE_SINGLE),a.isArray(b))return{options:b};var d={options:[]},e=0,g=0,h=f.MAX_PAGES;c.hasOwnProperty("maxPages")&&(h=c.maxPages);for(var i=1;i<=h;i++){var j=0;i<=2?(j=b,g=b):(g=2*g,j=g),e+=j;var k={itemcount:j,content:e};1===i&&(k.active=!0),d.options.push(k)}return d},o=function(a,b,c){var d=g();return c.hasOwnProperty("ignoreControlWhileLoading")&&(d.ignorecontrolwhileloading=c.ignoreControlWhileLoading),c.hasOwnProperty("controlPlacementBottom")&&(d.controlplacementbottom=c.controlPlacementBottom),c.hasOwnProperty("hideControlOnSinglePage")&&(d.hidecontrolonsinglepage=c.hideControlOnSinglePage),c.hasOwnProperty("ariaLabels")&&(d.arialabels=c.ariaLabels),c.hasOwnProperty("dropdown")&&c.dropdown?d.pagingdropdown=n(b,c):d.pagingbar=m(a,b),d},p=function(a,b){return r(null,null,a,b)},q=function(a,b,c){return r(null,a,b,c)},r=function(f,g,h,i){i=i||{};var j=a.Deferred(),k=o(f,g,i);return b.render(e.PAGED_CONTENT,k).then(function(b,c){b=a(b);var e=b;d.init(e,h),j.resolve(b,c)}).fail(function(a){j.reject(a)}).fail(c.exception),j.promise()},s=function(a,b,c,d){"undefined"==typeof d&&(d={});var e=a.length;return r(e,b,function(b){var d=[];return b.forEach(function(b){var c=b.offset,f=b.limit?c+b.limit:e,g=a.slice(c,f);d.push(g)}),c(d)},d)};return{create:p,createWithLimit:q,createWithTotalAndLimit:r,createFromStaticList:s,createFromAjax:r}});
|
2
lib/amd/build/paged_content_pages.min.js
vendored
2
lib/amd/build/paged_content_pages.min.js
vendored
|
@ -1 +1 @@
|
|||
define(["jquery","core/templates","core/notification","core/paged_content_events"],function(a,b,c,d){var e={ROOT:'[data-region="page-container"]',PAGE_REGION:'[data-region="paged-content-page"]',ACTIVE_PAGE_REGION:'[data-region="paged-content-page"].active'},f={PAGING_CONTENT_ITEM:"core/paged_content_page",LOADING:"core/overlay_loading"},g=function(a,b){return a.find('[data-page="'+b+'"]')},h=function(d){var e=a.Deferred();return b.render(f.LOADING,{visible:!0}).then(function(b){var c=a(b),f=setTimeout(function(){d.css("position","relative"),c.appendTo(d)},100);e.always(function(){clearTimeout(f),c.remove(),d.css("position","")})}).fail(c.exception),e},i=function(d,e,h){var i=a.Deferred();return e.then(function(a,e){b.render(f.PAGING_CONTENT_ITEM,{page:h,content:a}).then(function(a){b.appendNodeContents(d,a,e);var c=g(d,h);i.resolve(c)}).fail(function(a){i.reject(a)}).fail(c.exception)}).fail(function(a){i.reject(a)}).fail(c.exception),i},j=function(b,d,f){var j=[],k=[],l=a.Deferred();if(d.forEach(function(a){var c=a.pageNumber,d=g(b,c);d.length?j.push(d):k.push(a)}),k.length&&"function"==typeof f){var m=f(k),n=m.map(function(a,c){return i(b,a,k[c].pageNumber)});a.when.apply(a,n).then(function(){var a=Array.prototype.slice.call(arguments);l.resolve(a)}).fail(function(a){l.reject(a)}).fail(c.exception)}else l.resolve([]);var o=h(b);l.then(function(a){var c=j.concat(a);b.find(e.PAGE_REGION).addClass("hidden"),c.forEach(function(a){a.removeClass("hidden")})}).fail(c.exception).always(function(){o.resolve()})},k=function(b,c,e){b=a(b),c=a(c),c.on(d.SHOW_PAGES,function(a,c){j(b,c,e)})};return{init:k,rootSelector:e.ROOT}});
|
||||
define(["jquery","core/templates","core/notification","core/pubsub","core/paged_content_events"],function(a,b,c,d,e){var f={ROOT:'[data-region="page-container"]',PAGE_REGION:'[data-region="paged-content-page"]',ACTIVE_PAGE_REGION:'[data-region="paged-content-page"].active'},g={PAGING_CONTENT_ITEM:"core/paged_content_page",LOADING:"core/overlay_loading"},h=300,i=function(a,b){return a.find('[data-page="'+b+'"]')},j=function(d){var e=a.Deferred();return d.attr("aria-busy",!0),b.render(g.LOADING,{visible:!0}).then(function(b){var c=a(b),f=setTimeout(function(){d.css("position","relative"),c.appendTo(d)},h);e.always(function(){clearTimeout(f),c.remove(),d.css("position",""),d.removeAttr("aria-busy")})}).fail(c.exception),e},k=function(d,e,f){var h=a.Deferred();return e.then(function(a,e){e=e||"",b.render(g.PAGING_CONTENT_ITEM,{page:f,content:a}).then(function(a){b.appendNodeContents(d,a,e);var c=i(d,f);h.resolve(c)}).fail(function(a){h.reject(a)}).fail(c.exception)}).fail(function(a){h.reject(a)}).fail(c.exception),h.promise()},l=function(b,g,h,l){var m=[],n=[],o=a.Deferred();if(g.forEach(function(a){var c=a.pageNumber,d=i(b,c);d.length?m.push(d):n.push(a)}),n.length&&"function"==typeof l){var p=l(n,{allItemsLoaded:function(a){d.publish(h+e.ALL_ITEMS_LOADED,a)}}),q=p.map(function(a,c){return k(b,a,n[c].pageNumber)});a.when.apply(a,q).then(function(){var a=Array.prototype.slice.call(arguments);o.resolve(a)}).fail(function(a){o.reject(a)}).fail(c.exception)}else o.resolve([]);var r=j(b);o.then(function(a){var c=m.concat(a);b.find(f.PAGE_REGION).addClass("hidden"),c.forEach(function(a){a.removeClass("hidden")})}).then(function(){d.publish(h+e.PAGES_SHOWN,g)}).fail(c.exception).always(function(){r.resolve()})},m=function(b,c,f){b=a(b),d.subscribe(c+e.SHOW_PAGES,function(a){l(b,a,c,f)}),d.subscribe(c+e.SET_ITEMS_PER_PAGE_LIMIT,function(){b.empty()})};return{init:m,rootSelector:f.ROOT}});
|
|
@ -1 +1 @@
|
|||
define(["jquery","core/custom_interaction_events","core/paged_content_events"],function(a,b,c){var d={ROOT:'[data-region="paging-bar"]',PAGE:"[data-page]",PAGE_ITEM:'[data-region="page-item"]',ACTIVE_PAGE_ITEM:'[data-region="page-item"].active'},e=function(a,b){return a.find(d.PAGE_ITEM+'[data-page-number="'+b+'"]')},f=function(a){var b=a.find(d.PAGE).last();return b?parseInt(b.attr("data-page-number"),10):null},g=function(a){var b=a.find(d.ACTIVE_PAGE_ITEM);return b.length?h(a,b):null},h=function(a,b){if(void 0!=b.attr("data-page"))return parseInt(b.attr("data-page-number"),10);var c=1,d=null;switch(b.attr("data-control")){case"first":c=1;break;case"last":c=f(a);break;case"next":d=g(a);var e=f(a);c=d&&d<e?d+1:e;break;case"previous":d=g(a),c=d&&d>1?d-1:1;break;default:c=1}return parseInt(c,10)},i=function(a){return parseInt(a.attr("data-items-per-page"),10)},j=function(b){b.each(function(b,c){c=a(c),c.attr("data-page-number",b+1)})},k=function(a,b){var f=b==g(a),h=i(a),j=(b-1)*h;if(!f){a.find(d.PAGE_ITEM).removeClass("active");var k=e(a,b);k.addClass("active")}a.trigger(c.SHOW_PAGES,[[{pageNumber:b,limit:h,offset:j}]])},l=function(c){c=a(c);var e=c.find(d.PAGE);j(e);var f=g(c);f&&k(c,f),b.define(c,[b.events.activate]),c.on(b.events.activate,d.PAGE_ITEM,function(b,e){var f=a(b.target).closest(d.PAGE_ITEM),g=h(c,f);k(c,g),e.originalEvent.preventDefault(),e.originalEvent.stopPropagation()})};return{init:l,rootSelector:d.ROOT}});
|
||||
define(["jquery","core/custom_interaction_events","core/paged_content_events","core/str","core/pubsub"],function(a,b,c,d,e){var f={ROOT:'[data-region="paging-bar"]',PAGE:"[data-page]",PAGE_ITEM:'[data-region="page-item"]',PAGE_LINK:'[data-region="page-link"]',FIRST_BUTTON:'[data-control="first"]',LAST_BUTTON:'[data-control="last"]',NEXT_BUTTON:'[data-control="next"]',PREVIOUS_BUTTON:'[data-control="previous"]'},g=function(a,b){return a.find(f.PAGE_ITEM+'[data-page-number="'+b+'"]')},h=function(a){return a.find(f.NEXT_BUTTON)},i=function(a,b){a.attr("data-last-page-number",b)},j=function(a){return parseInt(a.attr("data-last-page-number"),10)},k=function(a){return parseInt(a.attr("data-active-page-number"),10)},l=function(a,b){a.attr("data-active-page-number",b)},m=function(a){var b=k(a);return!isNaN(b)&&0!=b},n=function(a,b){if(void 0!=b.attr("data-page"))return parseInt(b.attr("data-page-number"),10);var c=1,d=null;switch(b.attr("data-control")){case"first":c=1;break;case"last":c=j(a);break;case"next":d=k(a);var e=j(a);c=e?d&&d<e?d+1:e:d+1;break;case"previous":d=k(a),c=d&&d>1?d-1:1;break;default:c=1}return parseInt(c,10)},o=function(a){return parseInt(a.attr("data-items-per-page"),10)},p=function(a,b){a.attr("data-items-per-page",b)},q=function(a){a.removeClass("hidden")},r=function(a){a.addClass("hidden")},s=function(a){var b=a.find(f.NEXT_BUTTON),c=a.find(f.LAST_BUTTON);b.addClass("disabled"),b.attr("aria-disabled",!0),c.addClass("disabled"),c.attr("aria-disabled",!0)},t=function(a){var b=a.find(f.NEXT_BUTTON),c=a.find(f.LAST_BUTTON);b.removeClass("disabled"),b.removeAttr("aria-disabled"),c.removeClass("disabled"),c.removeAttr("aria-disabled")},u=function(a){var b=a.find(f.PREVIOUS_BUTTON),c=a.find(f.FIRST_BUTTON);b.addClass("disabled"),b.attr("aria-disabled",!0),c.addClass("disabled"),c.attr("aria-disabled",!0)},v=function(a){var b=a.find(f.PREVIOUS_BUTTON),c=a.find(f.FIRST_BUTTON);b.removeClass("disabled"),b.removeAttr("aria-disabled"),c.removeClass("disabled"),c.removeAttr("aria-disabled")},w=function(a){var b=a.attr("data-aria-label-components-pagination-item"),c=b.split(",").map(function(a){return a.trim()});return c},x=function(a){var b=a.attr("data-aria-label-components-pagination-active-item"),c=b.split(",").map(function(a){return a.trim()});return c},y=function(b,c){var d=0;l(b,0),c.each(function(c,e){var f=c+1;e=a(e),e.attr("data-page-number",f),d++,e.hasClass("active")&&l(b,f)}),i(b,d)},z=function(b){var c=w(b),e=x(b),g=k(b),h=b.find(f.PAGE_ITEM),i=h.map(function(d,f){f=a(f);var h=n(b,f);return h===g?{key:e[0],component:e[1],param:h}:{key:c[0],component:c[1],param:h}});d.get_strings(i).then(function(b){return h.each(function(c,d){d=a(d);var e=b[c];d.attr("aria-label",e),d.find(f.PAGE_LINK).attr("aria-label",e)}),b})["catch"](function(){})},A=function(a,b,d){var h=j(a),i=b==k(a),m=o(a),n=(b-1)*m;if(!i){a.find(f.PAGE_ITEM).removeClass("active").removeAttr("aria-current");var p=g(a,b);p.addClass("active"),p.attr("aria-current",!0),l(a,b)}h&&b>=h?s(a):t(a),b>1?v(a):u(a),z(a),e.publish(d+c.SHOW_PAGES,[{pageNumber:b,limit:m,offset:n}])},B=function(d,g){var h=d.attr("data-ignore-control-while-loading"),k=!1;""==h&&(h=!0),b.define(d,[b.events.activate]),d.on(b.events.activate,f.PAGE_ITEM,function(b,c){if(c.originalEvent.preventDefault(),c.originalEvent.stopPropagation(),!h||!k){var e=a(b.target).closest(f.PAGE_ITEM);if(!e.hasClass("disabled")){var i=n(d,e);A(d,i,g),k=!0}}}),e.subscribe(g+c.ALL_ITEMS_LOADED,function(a){k=!1;var b=j(d);(!b||a<b)&&i(d,a),1===a&&d.attr("data-hide-control-on-single-page")?(r(d),s(d),u(d)):(q(d),s(d))}),e.subscribe(g+c.PAGES_SHOWN,function(){k=!1}),e.subscribe(g+c.SET_ITEMS_PER_PAGE_LIMIT,function(a){p(d,a),i(d,0),l(d,0),q(d),A(d,1,g)})},C=function(b,c){b=a(b);var d=b.find(f.PAGE);if(y(b,d),B(b,c),m(b)){var e=k(b);g(b,e).click(),1==e&&u(b)}else h(b).click()};return{init:C,showPage:A,rootSelector:f.ROOT}});
|
1
lib/amd/build/paged_content_paging_bar_limit_selector.min.js
vendored
Normal file
1
lib/amd/build/paged_content_paging_bar_limit_selector.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
define(["jquery","core/custom_interaction_events","core/paged_content_events","core/pubsub"],function(a,b,c,d){var e={ROOT:'[data-region="paging-control-limit-container"]',LIMIT_OPTION:"[data-limit]",LIMIT_TOGGLE:'[data-action="limit-toggle"]'},f=function(f,g){f=a(f),b.define(f,[b.events.activate]),f.on(b.events.activate,e.LIMIT_OPTION,function(b,f){var h=a(b.target).closest(e.LIMIT_OPTION);if(!h.hasClass("active")){var i=parseInt(h.attr("data-limit"),10);d.publish(g+c.SET_ITEMS_PER_PAGE_LIMIT,i),f.originalEvent.preventDefault()}})};return{init:f,rootSelector:e.ROOT}});
|
|
@ -1 +1 @@
|
|||
define(["jquery","core/custom_interaction_events","core/paged_content_events"],function(a,b,c){var d={ROOT:'[data-region="paging-dropdown-container"]',DROPDOWN_ITEM:'[data-region="dropdown-item"]',DROPDOWN_TOGGLE:'[data-region="dropdown-toggle"]',ACTIVE_DROPDOWN_ITEM:'[data-region="dropdown-item"].active',CARET:'[data-region="caret"]'},e=function(a){return parseInt(a.attr("data-page-number"),10)},f=function(a){return a.find(d.DROPDOWN_ITEM)},g=function(b,c){var d=e(c);return f(b).filter(function(b,c){return e(a(c))<d})},h=function(a){return parseInt(a.attr("data-item-count"),10)},i=function(b,c){if(void 0!=c.attr("data-offset"))return parseInt(c.attr("data-offset"),10);var d=0;return g(b,c).each(function(b,c){c=a(c),d+=h(c)}),c.attr("data-offset",d),d},j=function(a){return a.find(d.ACTIVE_DROPDOWN_ITEM)},k=function(b,c){return c.map(function(c,d){return d=a(d),{pageNumber:e(d),limit:h(d),offset:i(b,d)}}).get()},l=function(b){b.each(function(b,c){c=a(c),c.attr("data-page-number",b+1)})},m=function(a,b){var e=g(a,b),f=e.add(b),h=k(a,f),i=a.find(d.DROPDOWN_TOGGLE),l=i.find(d.CARET);j(a).removeClass("active"),b.addClass("active"),i.html(b.text()),i.append(l),a.trigger(c.SHOW_PAGES,[h])},n=function(c){c=a(c);var e=f(c);l(e);var g=j(c);g.length&&m(c,g),b.define(c,[b.events.activate]),c.on(b.events.activate,d.DROPDOWN_ITEM,function(b,e){var f=a(b.target).closest(d.DROPDOWN_ITEM);m(c,f),e.originalEvent.preventDefault()})};return{init:n,rootSelector:d.ROOT}});
|
||||
define(["jquery","core/custom_interaction_events","core/paged_content_events","core/pubsub"],function(a,b,c,d){var e={ROOT:'[data-region="paging-dropdown-container"]',DROPDOWN_ITEM:'[data-region="dropdown-item"]',DROPDOWN_TOGGLE:'[data-region="dropdown-toggle"]',ACTIVE_DROPDOWN_ITEM:'[data-region="dropdown-item"].active',CARET:'[data-region="caret"]'},f=function(a){return parseInt(a.attr("data-page-number"),10)},g=function(a){return a.find(e.DROPDOWN_ITEM)},h=function(b,c){var d=f(c);return g(b).filter(function(b,c){return f(a(c))<d})},i=function(a){return parseInt(a.attr("data-item-count"),10)},j=function(b,c){if(void 0!=c.attr("data-offset"))return parseInt(c.attr("data-offset"),10);var d=0;return h(b,c).each(function(b,c){c=a(c),d+=i(c)}),c.attr("data-offset",d),d},k=function(a){return a.find(e.ACTIVE_DROPDOWN_ITEM)},l=function(b,c){return c.map(function(c,d){return d=a(d),{pageNumber:f(d),limit:i(d),offset:j(b,d)}}).get()},m=function(b){b.each(function(b,c){c=a(c),c.attr("data-page-number",b+1)})},n=function(a,b,f){var g=h(a,b),i=g.add(b),j=l(a,i),m=a.find(e.DROPDOWN_TOGGLE),n=m.find(e.CARET);k(a).removeClass("active"),b.addClass("active"),m.html(b.text()),m.append(n),d.publish(f+c.SHOW_PAGES,j)},o=function(c,d){c=a(c);var f=g(c);m(f);var h=k(c);h.length&&n(c,h,d),b.define(c,[b.events.activate]),c.on(b.events.activate,e.DROPDOWN_ITEM,function(b,f){var g=a(b.target).closest(e.DROPDOWN_ITEM);n(c,g,d),f.originalEvent.preventDefault()})};return{init:o,rootSelector:e.ROOT}});
|
1
lib/amd/build/pubsub.min.js
vendored
Normal file
1
lib/amd/build/pubsub.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
define([],function(){var a={},b=function(b,c){a[b]=a[b]||[],a[b].push(c)},c=function(b,c){if(a[b])for(var d=0;d<a[b].length;d++)if(a[b][d]===c){a[b].splice(d,1);break}},d=function(b,c){a[b]&&a[b].forEach(function(a){a(c)})};return{subscribe:b,unsubscribe:c,publish:d}});
|
2
lib/amd/build/user_date.min.js
vendored
2
lib/amd/build/user_date.min.js
vendored
|
@ -1 +1 @@
|
|||
define(["jquery","core/ajax","core/sessionstorage","core/config"],function(a,b,c,d){var e={},f=function(b){var c=a("html").attr("lang").replace(/-/g,"_");return"core_user_date/"+c+"/"+d.usertimezone+"/"+b.timestamp+"/"+b.format},g=function(a){return c.get(a)},h=function(a,b){c.set(a,b)},i=function(a){return"undefined"!=typeof e[a]},j=function(a){return e[a]},k=function(a,b){e[a]=b},l=function(a){var c=a.map(function(a){return{timestamp:a.timestamp,format:a.format}}),e={methodname:"core_get_user_dates",args:{contextid:d.contextid,timestamps:c}};return b.call([e],!0,!0)[0].then(function(b){b.dates.forEach(function(b,c){var d=a[c],e=f(d);h(e,b),d.deferred.resolve(b)})})["catch"](function(b){a.forEach(function(a){a.deferred.reject(b)})})},m=function(b){var c=[],d=[];return b.forEach(function(b){var e=f(b);if(i(e))d.push(j(e));else{var h=a.Deferred(),l=g(e);l?h.resolve(l):(b.deferred=h,c.push(b)),k(e,h.promise()),d.push(h.promise())}}),c.length&&l(c),a.when.apply(a,d).then(function(){return 1===arguments.length?[arguments[0]]:Array.apply(null,arguments)})};return{get:m}});
|
||||
define(["jquery","core/ajax","core/sessionstorage","core/config"],function(a,b,c,d){var e=86400,f={},g=function(b){var c=a("html").attr("lang").replace(/-/g,"_");return"core_user_date/"+c+"/"+d.usertimezone+"/"+b.timestamp+"/"+b.format},h=function(a){return c.get(a)},i=function(a,b){c.set(a,b)},j=function(a){return"undefined"!=typeof f[a]},k=function(a){return f[a]},l=function(a,b){f[a]=b},m=function(a){var c=a.map(function(a){return{timestamp:a.timestamp,format:a.format}}),e={methodname:"core_get_user_dates",args:{contextid:d.contextid,timestamps:c}};return b.call([e],!0,!0)[0].then(function(b){b.dates.forEach(function(b,c){var d=a[c],e=g(d);i(e,b),d.deferred.resolve(b)})})["catch"](function(b){a.forEach(function(a){a.deferred.reject(b)})})},n=function(b){var c=[],d=[];return b.forEach(function(b){var e=g(b);if(j(e))d.push(k(e));else{var f=a.Deferred(),i=h(e);i?f.resolve(i):(b.deferred=f,c.push(b)),l(e,f.promise()),d.push(f.promise())}}),c.length&&m(c),a.when.apply(a,d).then(function(){return 1===arguments.length?[arguments[0]]:Array.apply(null,arguments)})},o=function(a,b){var c=a>b,d=Math.abs(a-b),f=c?Math.floor(d/e):Math.ceil(d/e),g=f*e,h=c?b+g:b-g;return h};return{get:n,getUserMidnightForTimestamp:o}});
|
135
lib/amd/src/page_global.js
Normal file
135
lib/amd/src/page_global.js
Normal file
|
@ -0,0 +1,135 @@
|
|||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Provide global helper code to enhance page elements.
|
||||
*
|
||||
* @module core/page_global
|
||||
* @package core
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'core/custom_interaction_events',
|
||||
'core/str',
|
||||
],
|
||||
function(
|
||||
$,
|
||||
CustomEvents,
|
||||
Str
|
||||
) {
|
||||
|
||||
/**
|
||||
* Add an event handler for dropdown menus that wish to show their active item
|
||||
* in the dropdown toggle element.
|
||||
*
|
||||
* By default the handler will add the "active" class to the selected dropdown
|
||||
* item and set it's text as the HTML for the dropdown toggle.
|
||||
*
|
||||
* The behaviour of this handler is controlled by adding data attributes to
|
||||
* the HTML and requires the typically Bootstrap dropdown markup.
|
||||
*
|
||||
* data-show-active-item - Add to the .dropdown-menu element to enable default
|
||||
* functionality.
|
||||
* data-skip-active-class - Add to the .dropdown-menu to prevent this code from
|
||||
* adding the active class to the dropdown items
|
||||
* data-active-item-text - Add to an element within the data-toggle="dropdown" element
|
||||
* to use it as the active option text placeholder otherwise the
|
||||
* data-toggle="dropdown" element itself will be used.
|
||||
* data-active-item-button-aria-label-components - String components to set the aria
|
||||
* lable on the dropdown button. The string will be given the
|
||||
* active item text.
|
||||
*/
|
||||
var initActionOptionDropdownHandler = function() {
|
||||
var body = $('body');
|
||||
|
||||
CustomEvents.define(body, [CustomEvents.events.activate]);
|
||||
body.on(CustomEvents.events.activate, '[data-show-active-item]', function(e) {
|
||||
// The dropdown item that the user clicked on.
|
||||
var option = $(e.target).closest('.dropdown-item');
|
||||
// The dropdown menu element.
|
||||
var menuContainer = option.closest('[data-show-active-item]');
|
||||
|
||||
if (!option.hasClass('dropdown-item')) {
|
||||
// Ignore non Bootstrap dropdowns.
|
||||
return;
|
||||
}
|
||||
|
||||
if (option.hasClass('active')) {
|
||||
// If it's already active then we don't need to do anything.
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the active class from all other options.
|
||||
var dropdownItems = menuContainer.find('.dropdown-item');
|
||||
dropdownItems.removeClass('active');
|
||||
dropdownItems.removeAttr('aria-current');
|
||||
|
||||
if (!menuContainer.attr('data-skip-active-class')) {
|
||||
// Make this option active unless configured to ignore it.
|
||||
// Some code, for example the Bootstrap tabs, may want to handle
|
||||
// adding the active class itself.
|
||||
option.addClass('active');
|
||||
}
|
||||
|
||||
// Update aria attribute for active item.
|
||||
option.attr('aria-current', true);
|
||||
|
||||
var activeOptionText = option.text();
|
||||
var dropdownToggle = menuContainer.parent().find('[data-toggle="dropdown"]');
|
||||
var dropdownToggleText = dropdownToggle.find('[data-active-item-text]');
|
||||
|
||||
if (dropdownToggleText.length) {
|
||||
// We have a specific placeholder for the active item text so
|
||||
// use that.
|
||||
dropdownToggleText.html(activeOptionText);
|
||||
} else {
|
||||
// Otherwise just replace all of the toggle text with the active item.
|
||||
dropdownToggle.html(activeOptionText);
|
||||
}
|
||||
|
||||
var activeItemAriaLabelComponent = menuContainer.attr('data-active-item-button-aria-label-components');
|
||||
if (activeItemAriaLabelComponent) {
|
||||
// If we have string components for the aria label then load the string
|
||||
// and set the label on the dropdown toggle.
|
||||
var strParams = activeItemAriaLabelComponent.split(',');
|
||||
strParams.push(activeOptionText);
|
||||
|
||||
Str.get_string(strParams[0].trim(), strParams[1].trim(), strParams[2].trim())
|
||||
.then(function(string) {
|
||||
dropdownToggle.attr('aria-label', string);
|
||||
return string;
|
||||
})
|
||||
.catch(function() {
|
||||
// Silently ignore that we couldn't load the string.
|
||||
return false;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialise the global helper functions.
|
||||
*/
|
||||
var init = function() {
|
||||
initActionOptionDropdownHandler();
|
||||
};
|
||||
|
||||
return {
|
||||
init: init
|
||||
};
|
||||
});
|
75
lib/amd/src/paged_content.js
Normal file
75
lib/amd/src/paged_content.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Javascript to load and render a paged content section.
|
||||
*
|
||||
* @module core/paged_content
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'core/paged_content_pages',
|
||||
'core/paged_content_paging_bar',
|
||||
'core/paged_content_paging_bar_limit_selector',
|
||||
'core/paged_content_paging_dropdown'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
Pages,
|
||||
PagingBar,
|
||||
PagingBarLimitSelector,
|
||||
Dropdown
|
||||
) {
|
||||
|
||||
/**
|
||||
* Initialise the paged content region by running the pages
|
||||
* module and initialising any paging controls in the DOM.
|
||||
*
|
||||
* @param {object} root The paged content container element
|
||||
* @param {function} renderPagesContentCallback (optional) A callback function to render a
|
||||
* content page. See core/paged_content_pages for
|
||||
* more defails.
|
||||
*/
|
||||
var init = function(root, renderPagesContentCallback) {
|
||||
root = $(root);
|
||||
var pagesContainer = root.find(Pages.rootSelector);
|
||||
var pagingBarContainer = root.find(PagingBar.rootSelector);
|
||||
var dropdownContainer = root.find(Dropdown.rootSelector);
|
||||
var pagingBarLimitSelectorContainer = root.find(PagingBarLimitSelector.rootSelector);
|
||||
var id = root.attr('id');
|
||||
|
||||
Pages.init(pagesContainer, id, renderPagesContentCallback);
|
||||
|
||||
if (pagingBarContainer.length) {
|
||||
PagingBar.init(pagingBarContainer, id);
|
||||
}
|
||||
|
||||
if (pagingBarLimitSelectorContainer.length) {
|
||||
PagingBarLimitSelector.init(pagingBarLimitSelectorContainer, id);
|
||||
}
|
||||
|
||||
if (dropdownContainer.length) {
|
||||
Dropdown.init(dropdownContainer, id);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
init: init,
|
||||
rootSelector: '[data-region="paged-content-container"]'
|
||||
};
|
||||
});
|
|
@ -14,14 +14,17 @@
|
|||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Javascript to load and render the paging bar.
|
||||
* Events for the paged content element.
|
||||
*
|
||||
* @module core/paging_bar
|
||||
* @module core/paged_content_events
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define([], function() {
|
||||
return {
|
||||
SHOW_PAGES: 'core-paged-content-show-pages',
|
||||
PAGES_SHOWN: 'core-paged-content-pages-shown',
|
||||
ALL_ITEMS_LOADED: 'core-paged-content-all-items-loaded',
|
||||
SET_ITEMS_PER_PAGE_LIMIT: 'core-paged-content-set-items-per-page-limit'
|
||||
};
|
||||
});
|
||||
|
|
|
@ -25,7 +25,7 @@ define(
|
|||
'jquery',
|
||||
'core/templates',
|
||||
'core/notification',
|
||||
'core/paged_content_pages'
|
||||
'core/paged_content'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
|
@ -37,21 +37,92 @@ function(
|
|||
PAGED_CONTENT: 'core/paged_content'
|
||||
};
|
||||
|
||||
var DEFAULT = {
|
||||
ITEMS_PER_PAGE_SINGLE: 25,
|
||||
ITEMS_PER_PAGE_ARRAY: [25, 50, 100, 0],
|
||||
MAX_PAGES: 3
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the context to render the paging bar template with based on the number
|
||||
* of pages to show.
|
||||
* Get the default context to render the paged content mustache
|
||||
* template.
|
||||
*
|
||||
* @param {int} numberOfPages How many pages to have in the paging bar.
|
||||
* @param {int} itemsPerPage How many items will be shown per page.
|
||||
* @return {object} The template context.
|
||||
* @return {object}
|
||||
*/
|
||||
var buildPagingBarTemplateContext = function(numberOfPages, itemsPerPage) {
|
||||
var context = {
|
||||
"itemsperpage": itemsPerPage,
|
||||
"previous": {},
|
||||
"next": {},
|
||||
"pages": []
|
||||
var getDefaultTemplateContext = function() {
|
||||
return {
|
||||
pagingbar: false,
|
||||
pagingdropdown: false,
|
||||
skipjs: true,
|
||||
ignorecontrolwhileloading: true,
|
||||
controlplacementbottom: false
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the default context to render the paging bar mustache template.
|
||||
*
|
||||
* @return {object}
|
||||
*/
|
||||
var getDefaultPagingBarTemplateContext = function() {
|
||||
return {
|
||||
showitemsperpageselector: false,
|
||||
itemsperpage: 35,
|
||||
previous: true,
|
||||
next: true,
|
||||
activepagenumber: 1,
|
||||
hidecontrolonsinglepage: true,
|
||||
pages: []
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate the number of pages required for the given number of items and
|
||||
* how many of each item should appear on a page.
|
||||
*
|
||||
* @param {Number} numberOfItems How many items in total.
|
||||
* @param {Number} itemsPerPage How many items will be shown per page.
|
||||
* @return {Number} The number of pages required.
|
||||
*/
|
||||
var calculateNumberOfPages = function(numberOfItems, itemsPerPage) {
|
||||
var numberOfPages = 1;
|
||||
|
||||
if (numberOfItems > 0) {
|
||||
var partial = numberOfItems % itemsPerPage;
|
||||
|
||||
if (partial) {
|
||||
numberOfItems -= partial;
|
||||
numberOfPages = (numberOfItems / itemsPerPage) + 1;
|
||||
} else {
|
||||
numberOfPages = numberOfItems / itemsPerPage;
|
||||
}
|
||||
}
|
||||
|
||||
return numberOfPages;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the context for the paging bar template when we have a known number
|
||||
* of items.
|
||||
*
|
||||
* @param {Number} numberOfItems How many items in total.
|
||||
* @param {Number} itemsPerPage How many items will be shown per page.
|
||||
* @return {object} Mustache template
|
||||
*/
|
||||
var buildPagingBarTemplateContextKnownLength = function(numberOfItems, itemsPerPage) {
|
||||
if (itemsPerPage === null) {
|
||||
itemsPerPage = DEFAULT.ITEMS_PER_PAGE_SINGLE;
|
||||
}
|
||||
|
||||
if ($.isArray(itemsPerPage)) {
|
||||
// If we're given a total number of pages then we don't support a variable
|
||||
// set of items per page so just use the first one.
|
||||
itemsPerPage = itemsPerPage[0];
|
||||
}
|
||||
|
||||
var context = getDefaultPagingBarTemplateContext();
|
||||
context.itemsperpage = itemsPerPage;
|
||||
var numberOfPages = calculateNumberOfPages(numberOfItems, itemsPerPage);
|
||||
|
||||
for (var i = 1; i <= numberOfPages; i++) {
|
||||
var page = {
|
||||
|
@ -71,15 +142,101 @@ function(
|
|||
};
|
||||
|
||||
/**
|
||||
* Build the context to render the paging dropdown template with based on the number
|
||||
* Convert the itemsPerPage value into a format applicable for the mustache template.
|
||||
* The given value can be either a single integer or an array of integers / objects.
|
||||
*
|
||||
* E.g.
|
||||
* In: [5, 10]
|
||||
* out: [{value: 5, active: true}, {value: 10, active: false}]
|
||||
*
|
||||
* In: [5, {value: 10, active: true}]
|
||||
* Out: [{value: 5, active: false}, {value: 10, active: true}]
|
||||
*
|
||||
* In: [{value: 5, active: false}, {value: 10, active: true}]
|
||||
* Out: [{value: 5, active: false}, {value: 10, active: true}]
|
||||
*
|
||||
* @param {int|int[]} itemsPerPage Options for number of items per page.
|
||||
* @return {int|array}
|
||||
*/
|
||||
var buildItemsPerPagePagingBarContext = function(itemsPerPage) {
|
||||
if ($.isArray(itemsPerPage)) {
|
||||
// Convert the array into a format accepted by the template.
|
||||
var context = itemsPerPage.map(function(num) {
|
||||
if (typeof num === 'number') {
|
||||
// If the item is just a plain number then convert it into
|
||||
// an object with value and active keys.
|
||||
return {
|
||||
value: num,
|
||||
active: false
|
||||
};
|
||||
} else {
|
||||
// Otherwise we assume the caller has specified things correctly.
|
||||
return num;
|
||||
}
|
||||
});
|
||||
|
||||
var activeItems = context.filter(function(item) {
|
||||
return item.active;
|
||||
});
|
||||
|
||||
// Default the first item to active if one hasn't been specified.
|
||||
if (!activeItems.length) {
|
||||
context[0].active = true;
|
||||
}
|
||||
|
||||
return context;
|
||||
} else {
|
||||
return itemsPerPage;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the context for the paging bar template when we have an unknown
|
||||
* number of items.
|
||||
*
|
||||
* @param {Number} itemsPerPage How many items will be shown per page.
|
||||
* @return {object} Mustache template
|
||||
*/
|
||||
var buildPagingBarTemplateContextUnknownLength = function(itemsPerPage) {
|
||||
if (itemsPerPage === null) {
|
||||
itemsPerPage = DEFAULT.ITEMS_PER_PAGE_ARRAY;
|
||||
}
|
||||
|
||||
var context = getDefaultPagingBarTemplateContext();
|
||||
context.itemsperpage = buildItemsPerPagePagingBarContext(itemsPerPage);
|
||||
context.showitemsperpageselector = $.isArray(itemsPerPage);
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the context to render the paging bar template with based on the number
|
||||
* of pages to show.
|
||||
*
|
||||
* @param {int|null} numberOfItems How many items are there total.
|
||||
* @param {int|null} itemsPerPage How many items will be shown per page.
|
||||
* @return {object} The template context.
|
||||
*/
|
||||
var buildPagingBarTemplateContext = function(numberOfItems, itemsPerPage) {
|
||||
if (numberOfItems) {
|
||||
return buildPagingBarTemplateContextKnownLength(numberOfItems, itemsPerPage);
|
||||
} else {
|
||||
return buildPagingBarTemplateContextUnknownLength(itemsPerPage);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the context to render the paging dropdown template based on the number
|
||||
* of pages to show and items per page.
|
||||
*
|
||||
* This control is rendered with a gradual increase of the items per page to
|
||||
* limit the number of pages in the dropdown. Each page will show twice as much
|
||||
* as the previous page (except for the first two pages).
|
||||
*
|
||||
* By default there will only be 4 pages shown (including the "All" option) unless
|
||||
* a different number of pages is defined using the maxPages config value.
|
||||
*
|
||||
* For example:
|
||||
* Number of pages = 3
|
||||
* Items per page = 25
|
||||
* Would render a dropdown will 4 options:
|
||||
* 25
|
||||
|
@ -87,19 +244,30 @@ function(
|
|||
* 100
|
||||
* All
|
||||
*
|
||||
* @param {int} numberOfPages How many options to have in the dropdown.
|
||||
* @param {int} itemsPerPage How many items will be shown per page.
|
||||
* @param {Number} itemsPerPage How many items will be shown per page.
|
||||
* @param {object} config Configuration options provided by the client.
|
||||
* @return {object} The template context.
|
||||
*/
|
||||
var buildPagingDropdownTemplateContext = function(numberOfPages, itemsPerPage, config) {
|
||||
var buildPagingDropdownTemplateContext = function(itemsPerPage, config) {
|
||||
if (itemsPerPage === null) {
|
||||
itemsPerPage = DEFAULT.ITEMS_PER_PAGE_SINGLE;
|
||||
}
|
||||
|
||||
if ($.isArray(itemsPerPage)) {
|
||||
// If we're given an array for the items per page, rather than a number,
|
||||
// then just use that as the options for the dropdown.
|
||||
return {
|
||||
options: itemsPerPage
|
||||
};
|
||||
}
|
||||
|
||||
var context = {
|
||||
options: []
|
||||
};
|
||||
|
||||
var totalItems = 0;
|
||||
var lastIncrease = 0;
|
||||
var maxPages = numberOfPages;
|
||||
var maxPages = DEFAULT.MAX_PAGES;
|
||||
|
||||
if (config.hasOwnProperty('maxPages')) {
|
||||
maxPages = config.maxPages;
|
||||
|
@ -140,52 +308,39 @@ function(
|
|||
* By default the code will render a paging bar for the paging controls unless
|
||||
* otherwise specified in the provided config.
|
||||
*
|
||||
* @param {int} numberOfPages How many pages to have.
|
||||
* @param {int} itemsPerPage How many items will be shown per page.
|
||||
* @param {int|null} numberOfItems Total number of items.
|
||||
* @param {int|null|array} itemsPerPage How many items will be shown per page.
|
||||
* @param {object} config Configuration options provided by the client.
|
||||
* @return {object} The template context.
|
||||
*/
|
||||
var buildTemplateContext = function(numberOfPages, itemsPerPage, config) {
|
||||
var context = {
|
||||
pagingbar: false,
|
||||
pagingdropdown: false,
|
||||
skipjs: true
|
||||
};
|
||||
var buildTemplateContext = function(numberOfItems, itemsPerPage, config) {
|
||||
var context = getDefaultTemplateContext();
|
||||
|
||||
if (config.hasOwnProperty('ignoreControlWhileLoading')) {
|
||||
context.ignorecontrolwhileloading = config.ignoreControlWhileLoading;
|
||||
}
|
||||
|
||||
if (config.hasOwnProperty('controlPlacementBottom')) {
|
||||
context.controlplacementbottom = config.controlPlacementBottom;
|
||||
}
|
||||
|
||||
if (config.hasOwnProperty('hideControlOnSinglePage')) {
|
||||
context.hidecontrolonsinglepage = config.hideControlOnSinglePage;
|
||||
}
|
||||
|
||||
if (config.hasOwnProperty('ariaLabels')) {
|
||||
context.arialabels = config.ariaLabels;
|
||||
}
|
||||
|
||||
if (config.hasOwnProperty('dropdown') && config.dropdown) {
|
||||
context.pagingdropdown = buildPagingDropdownTemplateContext(numberOfPages, itemsPerPage, config);
|
||||
context.pagingdropdown = buildPagingDropdownTemplateContext(itemsPerPage, config);
|
||||
} else {
|
||||
context.pagingbar = buildPagingBarTemplateContext(numberOfPages, itemsPerPage);
|
||||
context.pagingbar = buildPagingBarTemplateContext(numberOfItems, itemsPerPage);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate the number of pages required for the given number of items and
|
||||
* how many of each item should appear on a page.
|
||||
*
|
||||
* @param {int} numberOfItems How many items in total.
|
||||
* @param {int} itemsPerPage How many items will be shown per page.
|
||||
* @return {int} The number of pages required.
|
||||
*/
|
||||
var calculateNumberOfPages = function(numberOfItems, itemsPerPage) {
|
||||
var numberOfPages = 1;
|
||||
|
||||
if (numberOfItems > 0) {
|
||||
var partial = numberOfItems % itemsPerPage;
|
||||
|
||||
if (partial) {
|
||||
numberOfItems -= partial;
|
||||
numberOfPages = (numberOfItems / itemsPerPage) + 1;
|
||||
} else {
|
||||
numberOfPages = numberOfItems / itemsPerPage;
|
||||
}
|
||||
}
|
||||
|
||||
return numberOfPages;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a paged content widget where the complete list of items is not loaded
|
||||
* up front but will instead be loaded by an ajax request (or similar).
|
||||
|
@ -198,30 +353,78 @@ function(
|
|||
*
|
||||
* The current list of configuration options available are:
|
||||
* dropdown {bool} True to render the page control as a dropdown (paging bar is default).
|
||||
* maxPages {Number} The maximum number of pages to show in the dropdown (only works with dropdown option)
|
||||
* ignoreControlWhileLoading {bool} Disable the pagination controls while loading a page (default to true)
|
||||
* controlPlacementBottom {bool} Render controls under paged content (default to false)
|
||||
*
|
||||
* @param {int} numberOfItems How many items are there in total.
|
||||
* @param {int} itemsPerPage How many items will be shown per page.
|
||||
* @param {function} renderPagesContentCallback Callback for loading and rendering the items.
|
||||
* @param {object} config Configuration options provided by the client.
|
||||
* @return {promise} Resolved with jQuery HTML and string JS.
|
||||
*/
|
||||
var createFromAjax = function(numberOfItems, itemsPerPage, renderPagesContentCallback, config) {
|
||||
if (typeof config == 'undefined') {
|
||||
config = {};
|
||||
}
|
||||
var create = function(renderPagesContentCallback, config) {
|
||||
return createWithTotalAndLimit(null, null, renderPagesContentCallback, config);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a paged content widget where the complete list of items is not loaded
|
||||
* up front but will instead be loaded by an ajax request (or similar).
|
||||
*
|
||||
* The client code must provide a callback function which loads and renders the
|
||||
* items for each page. See PagedContent.init for more details.
|
||||
*
|
||||
* The function will return a deferred that is resolved with a jQuery object
|
||||
* for the HTML content and a string for the JavaScript.
|
||||
*
|
||||
* The current list of configuration options available are:
|
||||
* dropdown {bool} True to render the page control as a dropdown (paging bar is default).
|
||||
* maxPages {Number} The maximum number of pages to show in the dropdown (only works with dropdown option)
|
||||
* ignoreControlWhileLoading {bool} Disable the pagination controls while loading a page (default to true)
|
||||
* controlPlacementBottom {bool} Render controls under paged content (default to false)
|
||||
*
|
||||
* @param {int|array|null} itemsPerPage How many items will be shown per page.
|
||||
* @param {function} renderPagesContentCallback Callback for loading and rendering the items.
|
||||
* @param {object} config Configuration options provided by the client.
|
||||
* @return {promise} Resolved with jQuery HTML and string JS.
|
||||
*/
|
||||
var createWithLimit = function(itemsPerPage, renderPagesContentCallback, config) {
|
||||
return createWithTotalAndLimit(null, itemsPerPage, renderPagesContentCallback, config);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a paged content widget where the complete list of items is not loaded
|
||||
* up front but will instead be loaded by an ajax request (or similar).
|
||||
*
|
||||
* The client code must provide a callback function which loads and renders the
|
||||
* items for each page. See PagedContent.init for more details.
|
||||
*
|
||||
* The function will return a deferred that is resolved with a jQuery object
|
||||
* for the HTML content and a string for the JavaScript.
|
||||
*
|
||||
* The current list of configuration options available are:
|
||||
* dropdown {bool} True to render the page control as a dropdown (paging bar is default).
|
||||
* maxPages {Number} The maximum number of pages to show in the dropdown (only works with dropdown option)
|
||||
* ignoreControlWhileLoading {bool} Disable the pagination controls while loading a page (default to true)
|
||||
* controlPlacementBottom {bool} Render controls under paged content (default to false)
|
||||
*
|
||||
* @param {int|null} numberOfItems How many items are there in total.
|
||||
* @param {int|array|null} itemsPerPage How many items will be shown per page.
|
||||
* @param {function} renderPagesContentCallback Callback for loading and rendering the items.
|
||||
* @param {object} config Configuration options provided by the client.
|
||||
* @return {promise} Resolved with jQuery HTML and string JS.
|
||||
*/
|
||||
var createWithTotalAndLimit = function(numberOfItems, itemsPerPage, renderPagesContentCallback, config) {
|
||||
config = config || {};
|
||||
|
||||
var deferred = $.Deferred();
|
||||
var numberOfPages = calculateNumberOfPages(numberOfItems, itemsPerPage);
|
||||
var templateContext = buildTemplateContext(numberOfPages, itemsPerPage, config);
|
||||
var templateContext = buildTemplateContext(numberOfItems, itemsPerPage, config);
|
||||
|
||||
Templates.render(TEMPLATES.PAGED_CONTENT, templateContext)
|
||||
.then(function(html, js) {
|
||||
html = $(html);
|
||||
|
||||
var container = html;
|
||||
var pagedContent = html.find(PagedContent.rootSelector);
|
||||
|
||||
PagedContent.init(pagedContent, container, renderPagesContentCallback);
|
||||
PagedContent.init(container, renderPagesContentCallback);
|
||||
|
||||
deferred.resolve(html, js);
|
||||
return;
|
||||
|
@ -231,7 +434,7 @@ function(
|
|||
})
|
||||
.fail(Notification.exception);
|
||||
|
||||
return deferred;
|
||||
return deferred.promise();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -247,9 +450,12 @@ function(
|
|||
*
|
||||
* The current list of configuration options available are:
|
||||
* dropdown {bool} True to render the page control as a dropdown (paging bar is default).
|
||||
* maxPages {Number} The maximum number of pages to show in the dropdown (only works with dropdown option)
|
||||
* ignoreControlWhileLoading {bool} Disable the pagination controls while loading a page (default to true)
|
||||
* controlPlacementBottom {bool} Render controls under paged content (default to false)
|
||||
*
|
||||
* @param {array} contentItems The list of items to paginate.
|
||||
* @param {int} itemsPerPage How many items will be shown per page.
|
||||
* @param {Number} itemsPerPage How many items will be shown per page.
|
||||
* @param {function} renderContentCallback Callback for rendering the items for the page.
|
||||
* @param {object} config Configuration options provided by the client.
|
||||
* @return {promise} Resolved with jQuery HTML and string JS.
|
||||
|
@ -260,7 +466,7 @@ function(
|
|||
}
|
||||
|
||||
var numberOfItems = contentItems.length;
|
||||
return createFromAjax(numberOfItems, itemsPerPage, function(pagesData) {
|
||||
return createWithTotalAndLimit(numberOfItems, itemsPerPage, function(pagesData) {
|
||||
var contentToRender = [];
|
||||
pagesData.forEach(function(pageData) {
|
||||
var begin = pageData.offset;
|
||||
|
@ -274,7 +480,11 @@ function(
|
|||
};
|
||||
|
||||
return {
|
||||
createFromAjax: createFromAjax,
|
||||
createFromStaticList: createFromStaticList
|
||||
create: create,
|
||||
createWithLimit: createWithLimit,
|
||||
createWithTotalAndLimit: createWithTotalAndLimit,
|
||||
createFromStaticList: createFromStaticList,
|
||||
// Backwards compatibility just in case anyone was using this.
|
||||
createFromAjax: createWithTotalAndLimit
|
||||
};
|
||||
});
|
||||
|
|
|
@ -25,12 +25,14 @@ define(
|
|||
'jquery',
|
||||
'core/templates',
|
||||
'core/notification',
|
||||
'core/pubsub',
|
||||
'core/paged_content_events'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
Templates,
|
||||
Notification,
|
||||
PubSub,
|
||||
PagedContentEvents
|
||||
) {
|
||||
|
||||
|
@ -45,6 +47,8 @@ define(
|
|||
LOADING: 'core/overlay_loading'
|
||||
};
|
||||
|
||||
var PRELOADING_GRACE_PERIOD = 300;
|
||||
|
||||
/**
|
||||
* Find a page by the number.
|
||||
*
|
||||
|
@ -60,23 +64,27 @@ define(
|
|||
* Show the loading spinner until the returned deferred is resolved by the
|
||||
* calling code.
|
||||
*
|
||||
* The loading spinner is only rendered after a short grace period to avoid
|
||||
* having it flash up briefly in the interface.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @returns {promise} The page.
|
||||
*/
|
||||
var startLoading = function(root) {
|
||||
var deferred = $.Deferred();
|
||||
root.attr('aria-busy', true);
|
||||
|
||||
Templates.render(TEMPLATES.LOADING, {visible: true})
|
||||
.then(function(html) {
|
||||
var loadingSpinner = $(html);
|
||||
// Put this in a timer to give the calling code 100 milliseconds
|
||||
// Put this in a timer to give the calling code 300 milliseconds
|
||||
// to render the content before we show the loading spinner. This
|
||||
// helps prevent a loading icon flicker on close to instant
|
||||
// rendering.
|
||||
var timerId = setTimeout(function() {
|
||||
root.css('position', 'relative');
|
||||
loadingSpinner.appendTo(root);
|
||||
}, 100);
|
||||
}, PRELOADING_GRACE_PERIOD);
|
||||
|
||||
deferred.always(function() {
|
||||
clearTimeout(timerId);
|
||||
|
@ -84,6 +92,7 @@ define(
|
|||
// by the calling code.
|
||||
loadingSpinner.remove();
|
||||
root.css('position', '');
|
||||
root.removeAttr('aria-busy');
|
||||
return;
|
||||
});
|
||||
|
||||
|
@ -102,12 +111,13 @@ define(
|
|||
*
|
||||
* @param {object} root The root element.
|
||||
* @param {promise} pagePromise The promise resolved with HTML and JS to render in the page.
|
||||
* @param {int} pageNumber The page number.
|
||||
* @param {Number} pageNumber The page number.
|
||||
* @returns {promise} The page.
|
||||
*/
|
||||
var renderPagePromise = function(root, pagePromise, pageNumber) {
|
||||
var deferred = $.Deferred();
|
||||
pagePromise.then(function(html, pageJS) {
|
||||
pageJS = pageJS || '';
|
||||
// When we get the contents to be rendered we can pass it in as the
|
||||
// content for a new page.
|
||||
Templates.render(TEMPLATES.PAGING_CONTENT_ITEM, {
|
||||
|
@ -135,7 +145,7 @@ define(
|
|||
})
|
||||
.fail(Notification.exception);
|
||||
|
||||
return deferred;
|
||||
return deferred.promise();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -164,11 +174,14 @@ define(
|
|||
* If the renderPagesContentCallback is not provided then it is assumed that
|
||||
* all pages have been rendered prior to initialising this module.
|
||||
*
|
||||
* This function triggers the PAGES_SHOWN event after the pages have been rendered.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @param {Number} pagesData The data for which pages need to be visible.
|
||||
* @param {string} id A unique id for this instance.
|
||||
* @param {function} renderPagesContentCallback Render pages content.
|
||||
*/
|
||||
var showPages = function(root, pagesData, renderPagesContentCallback) {
|
||||
var showPages = function(root, pagesData, id, renderPagesContentCallback) {
|
||||
var existingPages = [];
|
||||
var newPageData = [];
|
||||
var newPagesPromise = $.Deferred();
|
||||
|
@ -188,7 +201,11 @@ define(
|
|||
if (newPageData.length && typeof renderPagesContentCallback === 'function') {
|
||||
// If we have pages we haven't previously seen then ask the client code
|
||||
// to render them for us by calling the callback.
|
||||
var promises = renderPagesContentCallback(newPageData);
|
||||
var promises = renderPagesContentCallback(newPageData, {
|
||||
allItemsLoaded: function(lastPageNumber) {
|
||||
PubSub.publish(id + PagedContentEvents.ALL_ITEMS_LOADED, lastPageNumber);
|
||||
}
|
||||
});
|
||||
// After the client has finished rendering each of the pages being asked
|
||||
// for then begin our rendering process to put that content into paged
|
||||
// content pages.
|
||||
|
@ -229,6 +246,11 @@ define(
|
|||
|
||||
return;
|
||||
})
|
||||
.then(function() {
|
||||
// Let everything else know we've displayed the pages.
|
||||
PubSub.publish(id + PagedContentEvents.PAGES_SHOWN, pagesData);
|
||||
return;
|
||||
})
|
||||
.fail(Notification.exception)
|
||||
.always(function() {
|
||||
loadingPromise.resolve();
|
||||
|
@ -264,15 +286,20 @@ define(
|
|||
* The event element is the element to listen for the paged content events on.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @param {object} eventElement The element to listen for events on.
|
||||
* @param {string} id A unique id for this instance.
|
||||
* @param {function} renderPagesContentCallback Render pages content.
|
||||
*/
|
||||
var init = function(root, eventElement, renderPagesContentCallback) {
|
||||
var init = function(root, id, renderPagesContentCallback) {
|
||||
root = $(root);
|
||||
eventElement = $(eventElement);
|
||||
|
||||
eventElement.on(PagedContentEvents.SHOW_PAGES, function(e, pagesData) {
|
||||
showPages(root, pagesData, renderPagesContentCallback);
|
||||
PubSub.subscribe(id + PagedContentEvents.SHOW_PAGES, function(pagesData) {
|
||||
showPages(root, pagesData, id, renderPagesContentCallback);
|
||||
});
|
||||
|
||||
PubSub.subscribe(id + PagedContentEvents.SET_ITEMS_PER_PAGE_LIMIT, function() {
|
||||
// If the items per page limit was changed then we need to clear our content
|
||||
// the load new values based on the new limit.
|
||||
root.empty();
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -24,19 +24,27 @@ define(
|
|||
[
|
||||
'jquery',
|
||||
'core/custom_interaction_events',
|
||||
'core/paged_content_events'
|
||||
'core/paged_content_events',
|
||||
'core/str',
|
||||
'core/pubsub'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
CustomEvents,
|
||||
PagedContentEvents
|
||||
PagedContentEvents,
|
||||
Str,
|
||||
PubSub
|
||||
) {
|
||||
|
||||
var SELECTORS = {
|
||||
ROOT: '[data-region="paging-bar"]',
|
||||
PAGE: '[data-page]',
|
||||
PAGE_ITEM: '[data-region="page-item"]',
|
||||
ACTIVE_PAGE_ITEM: '[data-region="page-item"].active'
|
||||
PAGE_LINK: '[data-region="page-link"]',
|
||||
FIRST_BUTTON: '[data-control="first"]',
|
||||
LAST_BUTTON: '[data-control="last"]',
|
||||
NEXT_BUTTON: '[data-control="next"]',
|
||||
PREVIOUS_BUTTON: '[data-control="previous"]'
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -50,43 +58,74 @@ define(
|
|||
return root.find(SELECTORS.PAGE_ITEM + '[data-page-number="' + pageNumber + '"]');
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the next button element.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @return {jQuery}
|
||||
*/
|
||||
var getNextButton = function(root) {
|
||||
return root.find(SELECTORS.NEXT_BUTTON);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the last page number after which no more pages
|
||||
* should be loaded.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @param {Number} number Page number.
|
||||
*/
|
||||
var setLastPageNumber = function(root, number) {
|
||||
root.attr('data-last-page-number', number);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the last page number.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @return {int}
|
||||
* @return {Number}
|
||||
*/
|
||||
var getLastPageNumber = function(root) {
|
||||
var lastPage = root.find(SELECTORS.PAGE).last();
|
||||
if (lastPage) {
|
||||
return parseInt(lastPage.attr('data-page-number'), 10);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return parseInt(root.attr('data-last-page-number'), 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the active page number.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @returns {int} The page number
|
||||
* @returns {Number} The page number
|
||||
*/
|
||||
var getActivePageNumber = function(root) {
|
||||
var activePage = root.find(SELECTORS.ACTIVE_PAGE_ITEM);
|
||||
|
||||
if (activePage.length) {
|
||||
return getPageNumber(root, activePage);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return parseInt(root.attr('data-active-page-number'), 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the page number.
|
||||
* Set the active page number.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @param {object} page The page.
|
||||
* @returns {int} The page number
|
||||
* @param {Number} number Page number.
|
||||
*/
|
||||
var setActivePageNumber = function(root, number) {
|
||||
root.attr('data-active-page-number', number);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if there is an active page number.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @returns {bool}
|
||||
*/
|
||||
var hasActivePageNumber = function(root) {
|
||||
var number = getActivePageNumber(root);
|
||||
return !isNaN(number) && number != 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the page number for a given page.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @param {object} page The page element.
|
||||
* @returns {Number} The page number
|
||||
*/
|
||||
var getPageNumber = function(root, page) {
|
||||
if (page.attr('data-page') != undefined) {
|
||||
|
@ -110,7 +149,9 @@ define(
|
|||
case 'next':
|
||||
activePageNumber = getActivePageNumber(root);
|
||||
var lastPage = getLastPageNumber(root);
|
||||
if (activePageNumber && activePageNumber < lastPage) {
|
||||
if (!lastPage) {
|
||||
pageNumber = activePageNumber + 1;
|
||||
} else if (activePageNumber && activePageNumber < lastPage) {
|
||||
pageNumber = activePageNumber + 1;
|
||||
} else {
|
||||
pageNumber = lastPage;
|
||||
|
@ -139,22 +180,207 @@ define(
|
|||
* Get the limit of items for each page.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @returns {int}
|
||||
* @returns {Number}
|
||||
*/
|
||||
var getLimit = function(root) {
|
||||
return parseInt(root.attr('data-items-per-page'), 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the limit of items for each page.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @param {Number} limit Items per page limit.
|
||||
*/
|
||||
var setLimit = function(root, limit) {
|
||||
root.attr('data-items-per-page', limit);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the paging bar.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
*/
|
||||
var show = function(root) {
|
||||
root.removeClass('hidden');
|
||||
};
|
||||
|
||||
/**
|
||||
* Hide the paging bar.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
*/
|
||||
var hide = function(root) {
|
||||
root.addClass('hidden');
|
||||
};
|
||||
|
||||
/**
|
||||
* Disable the next and last buttons in the paging bar.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
*/
|
||||
var disableNextControlButtons = function(root) {
|
||||
var nextButton = root.find(SELECTORS.NEXT_BUTTON);
|
||||
var lastButton = root.find(SELECTORS.LAST_BUTTON);
|
||||
|
||||
nextButton.addClass('disabled');
|
||||
nextButton.attr('aria-disabled', true);
|
||||
lastButton.addClass('disabled');
|
||||
lastButton.attr('aria-disabled', true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable the next and last buttons in the paging bar.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
*/
|
||||
var enableNextControlButtons = function(root) {
|
||||
var nextButton = root.find(SELECTORS.NEXT_BUTTON);
|
||||
var lastButton = root.find(SELECTORS.LAST_BUTTON);
|
||||
|
||||
nextButton.removeClass('disabled');
|
||||
nextButton.removeAttr('aria-disabled');
|
||||
lastButton.removeClass('disabled');
|
||||
lastButton.removeAttr('aria-disabled');
|
||||
};
|
||||
|
||||
/**
|
||||
* Disable the previous and first buttons in the paging bar.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
*/
|
||||
var disablePreviousControlButtons = function(root) {
|
||||
var previousButton = root.find(SELECTORS.PREVIOUS_BUTTON);
|
||||
var firstButton = root.find(SELECTORS.FIRST_BUTTON);
|
||||
|
||||
previousButton.addClass('disabled');
|
||||
previousButton.attr('aria-disabled', true);
|
||||
firstButton.addClass('disabled');
|
||||
firstButton.attr('aria-disabled', true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable the previous and first buttons in the paging bar.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
*/
|
||||
var enablePreviousControlButtons = function(root) {
|
||||
var previousButton = root.find(SELECTORS.PREVIOUS_BUTTON);
|
||||
var firstButton = root.find(SELECTORS.FIRST_BUTTON);
|
||||
|
||||
previousButton.removeClass('disabled');
|
||||
previousButton.removeAttr('aria-disabled');
|
||||
firstButton.removeClass('disabled');
|
||||
firstButton.removeAttr('aria-disabled');
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the components for a get_string request for the aria-label
|
||||
* on a page. The value is a comma separated string of key and
|
||||
* component.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @return {array} First element is the key, second is the component.
|
||||
*/
|
||||
var getPageAriaLabelComponents = function(root) {
|
||||
var componentString = root.attr('data-aria-label-components-pagination-item');
|
||||
var components = componentString.split(',').map(function(component) {
|
||||
return component.trim();
|
||||
});
|
||||
return components;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the components for a get_string request for the aria-label
|
||||
* on an active page. The value is a comma separated string of key and
|
||||
* component.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @return {array} First element is the key, second is the component.
|
||||
*/
|
||||
var getActivePageAriaLabelComponents = function(root) {
|
||||
var componentString = root.attr('data-aria-label-components-pagination-active-item');
|
||||
var components = componentString.split(',').map(function(component) {
|
||||
return component.trim();
|
||||
});
|
||||
return components;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set page numbers on each of the given items. Page numbers are set
|
||||
* from 1..n (where n is the number of items).
|
||||
*
|
||||
* Sets the active page number to be the last page found with
|
||||
* an "active" class (if any).
|
||||
*
|
||||
* Sets the last page number.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @param {jQuery} items A jQuery list of items.
|
||||
*/
|
||||
var generatePageNumbers = function(items) {
|
||||
var generatePageNumbers = function(root, items) {
|
||||
var lastPageNumber = 0;
|
||||
setActivePageNumber(root, 0);
|
||||
|
||||
items.each(function(index, item) {
|
||||
var pageNumber = index + 1;
|
||||
item = $(item);
|
||||
item.attr('data-page-number', index + 1);
|
||||
item.attr('data-page-number', pageNumber);
|
||||
lastPageNumber++;
|
||||
|
||||
if (item.hasClass('active')) {
|
||||
setActivePageNumber(root, pageNumber);
|
||||
}
|
||||
});
|
||||
|
||||
setLastPageNumber(root, lastPageNumber);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the aria-labels on each of the page items in the paging bar.
|
||||
* This includes the next, previous, first, and last items.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
*/
|
||||
var generateAriaLabels = function(root) {
|
||||
var pageAriaLabelComponents = getPageAriaLabelComponents(root);
|
||||
var activePageAriaLabelComponents = getActivePageAriaLabelComponents(root);
|
||||
var activePageNumber = getActivePageNumber(root);
|
||||
var pageItems = root.find(SELECTORS.PAGE_ITEM);
|
||||
// We want to request all of the strings at once rather than
|
||||
// one at a time.
|
||||
var stringRequests = pageItems.map(function(index, page) {
|
||||
page = $(page);
|
||||
var pageNumber = getPageNumber(root, page);
|
||||
|
||||
if (pageNumber === activePageNumber) {
|
||||
return {
|
||||
key: activePageAriaLabelComponents[0],
|
||||
component: activePageAriaLabelComponents[1],
|
||||
param: pageNumber
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
key: pageAriaLabelComponents[0],
|
||||
component: pageAriaLabelComponents[1],
|
||||
param: pageNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Str.get_strings(stringRequests).then(function(strings) {
|
||||
pageItems.each(function(index, page) {
|
||||
page = $(page);
|
||||
var string = strings[index];
|
||||
page.attr('aria-label', string);
|
||||
page.find(SELECTORS.PAGE_LINK).attr('aria-label', string);
|
||||
});
|
||||
|
||||
return strings;
|
||||
})
|
||||
.catch(function() {
|
||||
// No need to interrupt the page if we can't load the aria lang strings.
|
||||
return;
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -164,10 +390,11 @@ define(
|
|||
* update.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @param {int} pageNumber The number for the page to show.
|
||||
* @param {object} page The page.
|
||||
* @param {Number} pageNumber The number for the page to show.
|
||||
* @param {string} id A uniqie id for this instance.
|
||||
*/
|
||||
var showPage = function(root, pageNumber) {
|
||||
var showPage = function(root, pageNumber, id) {
|
||||
var lastPageNumber = getLastPageNumber(root);
|
||||
var isSamePage = pageNumber == getActivePageNumber(root);
|
||||
var limit = getLimit(root);
|
||||
var offset = (pageNumber - 1) * limit;
|
||||
|
@ -175,36 +402,56 @@ define(
|
|||
if (!isSamePage) {
|
||||
// We only need to toggle the active class if the user didn't click
|
||||
// on the already active page.
|
||||
root.find(SELECTORS.PAGE_ITEM).removeClass('active');
|
||||
root.find(SELECTORS.PAGE_ITEM).removeClass('active').removeAttr('aria-current');
|
||||
var page = getPageByNumber(root, pageNumber);
|
||||
page.addClass('active');
|
||||
page.attr('aria-current', true);
|
||||
setActivePageNumber(root, pageNumber);
|
||||
}
|
||||
|
||||
// Make sure the control buttons are disabled as the user navigates
|
||||
// to either end of the limits.
|
||||
if (lastPageNumber && pageNumber >= lastPageNumber) {
|
||||
disableNextControlButtons(root);
|
||||
} else {
|
||||
enableNextControlButtons(root);
|
||||
}
|
||||
|
||||
if (pageNumber > 1) {
|
||||
enablePreviousControlButtons(root);
|
||||
} else {
|
||||
disablePreviousControlButtons(root);
|
||||
}
|
||||
|
||||
generateAriaLabels(root);
|
||||
|
||||
// This event requires a payload that contains a list of all pages that
|
||||
// were activated. In the case of the paging bar we only show one page at
|
||||
// a time.
|
||||
root.trigger(PagedContentEvents.SHOW_PAGES, [[{
|
||||
PubSub.publish(id + PagedContentEvents.SHOW_PAGES, [{
|
||||
pageNumber: pageNumber,
|
||||
limit: limit,
|
||||
offset: offset
|
||||
}]]);
|
||||
}]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialise the paging bar.
|
||||
* Add event listeners for interactions with the paging bar as well as listening
|
||||
* for custom paged content events.
|
||||
*
|
||||
* Each event will trigger different logic to update parts of the paging bar's
|
||||
* display.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @param {string} id A uniqie id for this instance.
|
||||
*/
|
||||
var init = function(root) {
|
||||
root = $(root);
|
||||
var pages = root.find(SELECTORS.PAGE);
|
||||
generatePageNumbers(pages);
|
||||
var registerEventListeners = function(root, id) {
|
||||
var ignoreControlWhileLoading = root.attr('data-ignore-control-while-loading');
|
||||
var loading = false;
|
||||
|
||||
var activePageNumber = getActivePageNumber(root);
|
||||
if (activePageNumber) {
|
||||
// If the the paging bar was rendered with an active page selected
|
||||
// then make sure we fired off the event to tell the content page to
|
||||
// show.
|
||||
showPage(root, activePageNumber);
|
||||
if (ignoreControlWhileLoading == "") {
|
||||
// Default to ignoring control while loading if not specified.
|
||||
ignoreControlWhileLoading = true;
|
||||
}
|
||||
|
||||
CustomEvents.define(root, [
|
||||
|
@ -212,17 +459,98 @@ define(
|
|||
]);
|
||||
|
||||
root.on(CustomEvents.events.activate, SELECTORS.PAGE_ITEM, function(e, data) {
|
||||
var page = $(e.target).closest(SELECTORS.PAGE_ITEM);
|
||||
var pageNumber = getPageNumber(root, page);
|
||||
showPage(root, pageNumber);
|
||||
|
||||
data.originalEvent.preventDefault();
|
||||
data.originalEvent.stopPropagation();
|
||||
|
||||
if (ignoreControlWhileLoading && loading) {
|
||||
// Do nothing if configured to ignore control while loading.
|
||||
return;
|
||||
}
|
||||
|
||||
var page = $(e.target).closest(SELECTORS.PAGE_ITEM);
|
||||
|
||||
if (!page.hasClass('disabled')) {
|
||||
var pageNumber = getPageNumber(root, page);
|
||||
showPage(root, pageNumber, id);
|
||||
loading = true;
|
||||
}
|
||||
});
|
||||
|
||||
// This event is fired when all of the items have been loaded. Typically used
|
||||
// in an "infinite" pages context when we don't know the exact number of pages
|
||||
// ahead of time.
|
||||
PubSub.subscribe(id + PagedContentEvents.ALL_ITEMS_LOADED, function(pageNumber) {
|
||||
loading = false;
|
||||
var currentLastPage = getLastPageNumber(root);
|
||||
|
||||
if (!currentLastPage || pageNumber < currentLastPage) {
|
||||
// Somehow the value we've got saved is higher than the new
|
||||
// value we just received. Perhaps events came out of order.
|
||||
// In any case, save the lowest value.
|
||||
setLastPageNumber(root, pageNumber);
|
||||
}
|
||||
|
||||
if (pageNumber === 1 && root.attr('data-hide-control-on-single-page')) {
|
||||
// If all items were loaded on the first page then we can hide
|
||||
// the paging bar because there are no other pages to load.
|
||||
hide(root);
|
||||
disableNextControlButtons(root);
|
||||
disablePreviousControlButtons(root);
|
||||
} else {
|
||||
show(root);
|
||||
disableNextControlButtons(root);
|
||||
}
|
||||
});
|
||||
|
||||
// This event is fired after all of the requested pages have been rendered.
|
||||
PubSub.subscribe(id + PagedContentEvents.PAGES_SHOWN, function() {
|
||||
// All pages have been shown so turn off the loading flag.
|
||||
loading = false;
|
||||
});
|
||||
|
||||
// This is triggered when the paging limit is modified.
|
||||
PubSub.subscribe(id + PagedContentEvents.SET_ITEMS_PER_PAGE_LIMIT, function(limit) {
|
||||
// Update the limit.
|
||||
setLimit(root, limit);
|
||||
setLastPageNumber(root, 0);
|
||||
setActivePageNumber(root, 0);
|
||||
show(root);
|
||||
// Reload the data from page 1 again.
|
||||
showPage(root, 1, id);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialise the paging bar.
|
||||
* @param {object} root The root element.
|
||||
* @param {string} id A uniqie id for this instance.
|
||||
*/
|
||||
var init = function(root, id) {
|
||||
root = $(root);
|
||||
var pages = root.find(SELECTORS.PAGE);
|
||||
generatePageNumbers(root, pages);
|
||||
registerEventListeners(root, id);
|
||||
|
||||
if (hasActivePageNumber(root)) {
|
||||
var activePageNumber = getActivePageNumber(root);
|
||||
// If the the paging bar was rendered with an active page selected
|
||||
// then make sure we fired off the event to tell the content page to
|
||||
// show.
|
||||
getPageByNumber(root, activePageNumber).click();
|
||||
if (activePageNumber == 1) {
|
||||
// If the first page is active then disable the previous buttons.
|
||||
disablePreviousControlButtons(root);
|
||||
}
|
||||
} else {
|
||||
// There was no active page number so load the first page using
|
||||
// the next button. This allows the infinite pagination to work.
|
||||
getNextButton(root).click();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
init: init,
|
||||
showPage: showPage,
|
||||
rootSelector: SELECTORS.ROOT,
|
||||
};
|
||||
});
|
||||
|
|
77
lib/amd/src/paged_content_paging_bar_limit_selector.js
Normal file
77
lib/amd/src/paged_content_paging_bar_limit_selector.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Javascript for dynamically changing the page limits.
|
||||
*
|
||||
* @module core/paged_content_paging_bar_limit_selector
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'core/custom_interaction_events',
|
||||
'core/paged_content_events',
|
||||
'core/pubsub'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
CustomEvents,
|
||||
PagedContentEvents,
|
||||
PubSub
|
||||
) {
|
||||
|
||||
var SELECTORS = {
|
||||
ROOT: '[data-region="paging-control-limit-container"]',
|
||||
LIMIT_OPTION: '[data-limit]',
|
||||
LIMIT_TOGGLE: '[data-action="limit-toggle"]',
|
||||
};
|
||||
|
||||
/**
|
||||
* Trigger the SET_ITEMS_PER_PAGE_LIMIT event when the page limit option
|
||||
* is modified.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @param {string} id A unique id for this instance.
|
||||
*/
|
||||
var init = function(root, id) {
|
||||
root = $(root);
|
||||
|
||||
CustomEvents.define(root, [
|
||||
CustomEvents.events.activate
|
||||
]);
|
||||
|
||||
root.on(CustomEvents.events.activate, SELECTORS.LIMIT_OPTION, function(e, data) {
|
||||
var optionElement = $(e.target).closest(SELECTORS.LIMIT_OPTION);
|
||||
|
||||
if (optionElement.hasClass('active')) {
|
||||
// Don't do anything if it was the active option selected.
|
||||
return;
|
||||
}
|
||||
|
||||
var limit = parseInt(optionElement.attr('data-limit'), 10);
|
||||
// Tell the rest of the pagination components that the limit has changed.
|
||||
PubSub.publish(id + PagedContentEvents.SET_ITEMS_PER_PAGE_LIMIT, limit);
|
||||
|
||||
data.originalEvent.preventDefault();
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
init: init,
|
||||
rootSelector: SELECTORS.ROOT
|
||||
};
|
||||
});
|
|
@ -24,12 +24,14 @@ define(
|
|||
[
|
||||
'jquery',
|
||||
'core/custom_interaction_events',
|
||||
'core/paged_content_events'
|
||||
'core/paged_content_events',
|
||||
'core/pubsub'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
CustomEvents,
|
||||
PagedContentEvents
|
||||
PagedContentEvents,
|
||||
PubSub
|
||||
) {
|
||||
|
||||
var SELECTORS = {
|
||||
|
@ -44,7 +46,7 @@ define(
|
|||
* Get the page number.
|
||||
*
|
||||
* @param {jquery} item The dropdown item.
|
||||
* @returns {int}
|
||||
* @returns {Number}
|
||||
*/
|
||||
var getPageNumber = function(item) {
|
||||
return parseInt(item.attr('data-page-number'), 10);
|
||||
|
@ -79,7 +81,7 @@ define(
|
|||
* Get the number of items to be loaded for the dropdown item.
|
||||
*
|
||||
* @param {jquery} item The dropdown item.
|
||||
* @returns {int}
|
||||
* @returns {Number}
|
||||
*/
|
||||
var getLimit = function(item) {
|
||||
return parseInt(item.attr('data-item-count'), 10);
|
||||
|
@ -91,7 +93,7 @@ define(
|
|||
*
|
||||
* @param {jquery} root The root element.
|
||||
* @param {jquery} item The dropdown item.
|
||||
* @returns {int}
|
||||
* @returns {Number}
|
||||
*/
|
||||
var getOffset = function(root, item) {
|
||||
if (item.attr('data-offset') != undefined) {
|
||||
|
@ -181,8 +183,9 @@ define(
|
|||
*
|
||||
* @param {jquery} root The root element.
|
||||
* @param {jquery} item The dropdown item.
|
||||
* @param {string} id A unique id for this instance.
|
||||
*/
|
||||
var setActiveItem = function(root, item) {
|
||||
var setActiveItem = function(root, item, id) {
|
||||
var prevItems = getPreviousItems(root, item);
|
||||
var allItems = prevItems.add(item);
|
||||
var eventPayload = generateEventPayload(root, allItems);
|
||||
|
@ -197,7 +200,7 @@ define(
|
|||
// Bootstrap 2 compatibility.
|
||||
toggle.append(caret);
|
||||
// Fire the event to tell the content to update.
|
||||
root.trigger(PagedContentEvents.SHOW_PAGES, [eventPayload]);
|
||||
PubSub.publish(id + PagedContentEvents.SHOW_PAGES, eventPayload);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -206,8 +209,9 @@ define(
|
|||
* new pages.
|
||||
*
|
||||
* @param {object} root The root element.
|
||||
* @param {string} id A unique id for this instance.
|
||||
*/
|
||||
var init = function(root) {
|
||||
var init = function(root, id) {
|
||||
root = $(root);
|
||||
var items = getAllItems(root);
|
||||
generatePageNumbers(items);
|
||||
|
@ -215,7 +219,7 @@ define(
|
|||
var activeItem = getActiveItem(root);
|
||||
if (activeItem.length) {
|
||||
// Fire the first event for the content to make sure it's visible.
|
||||
setActiveItem(root, activeItem);
|
||||
setActiveItem(root, activeItem, id);
|
||||
}
|
||||
|
||||
CustomEvents.define(root, [
|
||||
|
@ -224,7 +228,7 @@ define(
|
|||
|
||||
root.on(CustomEvents.events.activate, SELECTORS.DROPDOWN_ITEM, function(e, data) {
|
||||
var item = $(e.target).closest(SELECTORS.DROPDOWN_ITEM);
|
||||
setActiveItem(root, item);
|
||||
setActiveItem(root, item, id);
|
||||
|
||||
data.originalEvent.preventDefault();
|
||||
});
|
||||
|
|
74
lib/amd/src/pubsub.js
Normal file
74
lib/amd/src/pubsub.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* A simple Javascript PubSub implementation.
|
||||
*
|
||||
* @module core/pubsub
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define([], function() {
|
||||
|
||||
var events = {};
|
||||
|
||||
/**
|
||||
* Subscribe to an event.
|
||||
*
|
||||
* @param {string} eventName The name of the event to subscribe to.
|
||||
* @param {function} callback The callback function to run when eventName occurs.
|
||||
*/
|
||||
var subscribe = function(eventName, callback) {
|
||||
events[eventName] = events[eventName] || [];
|
||||
events[eventName].push(callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Unsubscribe from an event.
|
||||
*
|
||||
* @param {string} eventName The name of the event to unsubscribe from.
|
||||
* @param {function} callback The callback to unsubscribe.
|
||||
*/
|
||||
var unsubscribe = function(eventName, callback) {
|
||||
if (events[eventName]) {
|
||||
for (var i = 0; i < events[eventName].length; i++) {
|
||||
if (events[eventName][i] === callback) {
|
||||
events[eventName].splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Publish an event to all subscribers.
|
||||
*
|
||||
* @param {string} eventName The name of the event to publish.
|
||||
* @param {any} data The data to provide to the subscribed callbacks.
|
||||
*/
|
||||
var publish = function(eventName, data) {
|
||||
if (events[eventName]) {
|
||||
events[eventName].forEach(function(callback) {
|
||||
callback(data);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
subscribe: subscribe,
|
||||
unsubscribe: unsubscribe,
|
||||
publish: publish
|
||||
};
|
||||
});
|
|
@ -24,6 +24,8 @@
|
|||
define(['jquery', 'core/ajax', 'core/sessionstorage', 'core/config'],
|
||||
function($, Ajax, Storage, Config) {
|
||||
|
||||
var SECONDS_IN_DAY = 86400;
|
||||
|
||||
/** @var {object} promisesCache Store all promises we've seen so far. */
|
||||
var promisesCache = {};
|
||||
|
||||
|
@ -228,7 +230,42 @@ define(['jquery', 'core/ajax', 'core/sessionstorage', 'core/config'],
|
|||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* For a given timestamp get the midnight value in the user's timezone.
|
||||
*
|
||||
* The calculation is performed relative to the user's midnight timestamp
|
||||
* for today to ensure that timezones are preserved.
|
||||
*
|
||||
* E.g.
|
||||
* Input:
|
||||
* timestamp: 1514836800 (01/01/2018 8pm GMT)(02/01/2018 4am GMT+8)
|
||||
* midnight: 1514851200 (02/01/2018 midnight GMT)
|
||||
* Output:
|
||||
* 1514764800 (01/01/2018 midnight GMT)
|
||||
*
|
||||
* Input:
|
||||
* timestamp: 1514836800 (01/01/2018 8pm GMT)(02/01/2018 4am GMT+8)
|
||||
* midnight: 1514822400 (02/01/2018 midnight GMT+8)
|
||||
* Output:
|
||||
* 1514822400 (02/01/2018 midnight GMT+8)
|
||||
*
|
||||
* @param {Number} timestamp The timestamp to calculate from
|
||||
* @param {Number} todayMidnight The user's midnight timestamp
|
||||
* @return {Number} The midnight value of the user's timestamp
|
||||
*/
|
||||
var getUserMidnightForTimestamp = function(timestamp, todayMidnight) {
|
||||
var future = timestamp > todayMidnight;
|
||||
var diffSeconds = Math.abs(timestamp - todayMidnight);
|
||||
var diffDays = future ? Math.floor(diffSeconds / SECONDS_IN_DAY) : Math.ceil(diffSeconds / SECONDS_IN_DAY);
|
||||
var diffDaysInSeconds = diffDays * SECONDS_IN_DAY;
|
||||
// Is the timestamp in the future or past?
|
||||
var dayTimestamp = future ? todayMidnight + diffDaysInSeconds : todayMidnight - diffDaysInSeconds;
|
||||
return dayTimestamp;
|
||||
};
|
||||
|
||||
return {
|
||||
get: get
|
||||
get: get,
|
||||
getUserMidnightForTimestamp: getUserMidnightForTimestamp
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -260,6 +260,7 @@ class icon_system_fontawesome extends icon_system_font {
|
|||
'core:i/ne_red_mark' => 'fa-remove',
|
||||
'core:i/new' => 'fa-bolt',
|
||||
'core:i/news' => 'fa-newspaper-o',
|
||||
'core:i/next' => 'fa-chevron-right',
|
||||
'core:i/nosubcat' => 'fa-plus-square-o',
|
||||
'core:i/notifications' => 'fa-bell',
|
||||
'core:i/open' => 'fa-folder-open',
|
||||
|
@ -270,6 +271,7 @@ class icon_system_fontawesome extends icon_system_font {
|
|||
'core:i/persona_sign_in_black' => 'fa-male',
|
||||
'core:i/portfolio' => 'fa-id-badge',
|
||||
'core:i/preview' => 'fa-search-plus',
|
||||
'core:i/previous' => 'fa-chevron-left',
|
||||
'core:i/privatefiles' => 'fa-file-o',
|
||||
'core:i/progressbar' => 'fa-spinner fa-spin',
|
||||
'core:i/publish' => 'fa-share',
|
||||
|
|
|
@ -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(
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue