%PDF- %PDF-
Direktori : /home/vacivi36/ava/blocks/timeline/amd/src/ |
Current File : /home/vacivi36/ava/blocks/timeline/amd/src/view_courses.js |
// 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. * * @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/templates', 'block_timeline/event_list', 'core_course/repository', 'block_timeline/calendar_events_repository', 'core/pending' ], function( $, Notification, CustomEvents, Templates, EventList, CourseRepository, EventsRepository, Pending ) { var SELECTORS = { MORE_COURSES_BUTTON: '[data-action="more-courses"]', MORE_COURSES_BUTTON_CONTAINER: '[data-region="more-courses-button-container"]', NO_COURSES_EMPTY_MESSAGE: '[data-region="no-courses-empty-message"]', NO_COURSES_WITH_EVENTS_MESSAGE: '[data-region="no-events-empty-message"]', COURSES_LIST: '[data-region="courses-list"]', COURSE_ITEMS_LOADING_PLACEHOLDER: '[data-region="course-items-loading-placeholder"]', COURSE_EVENTS_CONTAINER: '[data-region="course-events-container"]', COURSE_NAME: '[data-region="course-name"]', LOADING_ICON: '.loading-icon', TIMELINE_BLOCK: '[data-region="timeline"]', TIMELINE_SEARCH: '[data-action="search"]' }; var TEMPLATES = { COURSE_ITEMS: 'block_timeline/course-items', LOADING_ICON: 'core/loading' }; var COURSE_CLASSIFICATION = 'all'; var COURSE_SORT = 'fullname asc'; var COURSE_EVENT_LIMIT = 5; var COURSE_LIMIT = 2; var SECONDS_IN_DAY = 60 * 60 * 24; const additionalConfig = {courseview: true}; /** * Hide the loading placeholder elements. * * @param {object} root The rool element. */ var hideLoadingPlaceholder = function(root) { root.find(SELECTORS.COURSE_ITEMS_LOADING_PLACEHOLDER).addClass('hidden'); }; /** * Show the loading placeholder elements. * * @param {object} root The rool element. */ const showLoadingPlaceholder = function(root) { root.find(SELECTORS.COURSE_ITEMS_LOADING_PLACEHOLDER).removeClass('hidden'); }; /** * Hide the "more courses" button. * * @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 courses have no events available (within the current filtering). * * @param {object} root The rool element. */ const showNoCoursesWithEventsMessage = function(root) { // Remove any course list contents, since we will display the no events message. const container = root.find(SELECTORS.COURSES_LIST); Templates.replaceNodeContents(container, '', ''); root.find(SELECTORS.NO_COURSES_WITH_EVENTS_MESSAGE).removeClass('hidden'); }; /** * Hide the message for when courses have no events available (within the current filtering). * * @param {object} root The rool element. */ const hideNoCoursesWithEventsMessage = function(root) { root.find(SELECTORS.NO_COURSES_WITH_EVENTS_MESSAGE).addClass('hidden'); }; /** * Render the course items HTML to the page. * * @param {object} root The rool element. * @param {string} html The course items HTML to render. * @param {boolean} append Whether the HTML should be appended (eg pressed "show more courses"). * Defaults to false - replaces the existing content (eg when modifying filter values). */ var renderCourseItemsHTML = function(root, html, append = false) { var container = root.find(SELECTORS.COURSES_LIST); if (append) { Templates.appendNodeContents(container, html, ''); } else { Templates.replaceNodeContents(container, html, ''); } }; /** * 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, unless filtering by overdue, where the current UNIX timestamp is used. * * @param {object} root The rool element. * @return {Number} */ var getEndTime = function(root) { let endTime = null; if (root.attr('data-filter-overdue')) { // If filtering by overdue, end time will be the current timestamp in seconds. endTime = Math.floor(Date.now() / 1000); } else { const midnight = getMidnight(root); const daysLimit = getDaysLimit(root); if (daysLimit != undefined) { endTime = midnight + (daysLimit * SECONDS_IN_DAY); } } return endTime; }; /** * 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. * @param {string|undefined} searchValue Search value * @return {object} jQuery promise. */ var getEventsForCourseIds = function(courseIds, startTime, limit, endTime, searchValue) { var args = { courseids: courseIds, starttime: startTime, limit: limit }; if (endTime) { args.endtime = endTime; } if (searchValue) { args.searchvalue = searchValue; } 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. * @param {string|undefined} searchValue Search value * @return {object} jQuery promise resolved with the events. */ var loadEventsForCourses = function(courses, startTime, endTime, searchValue) { var courseIds = courses.map(function(course) { return course.id; }); return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime, searchValue); }; /** * 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 {boolean} append Whether new content should be appended instead of replaced (eg "show more courses"). * @return {object} jQuery promise resolved after rendering is complete. */ var updateDisplayFromCourses = function(courses, root, midnight, daysOffset, daysLimit, append) { // 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, courseview: true, hascourses: true }).then(function(html) { hideLoadingPlaceholder(root); if (html) { // Template rendering is complete and we have the HTML so we can // add it to the DOM. renderCourseItemsHTML(root, html, append); } 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. * @param {boolean} append Whether content should be appended instead of replaced (eg "show more courses"). False by default. * @return {object} jQuery promise resolved with courses and events. */ var loadMoreCourses = function(root, append = false) { const pendingPromise = new Pending('block/timeline:load-more-courses'); var offset = getOffset(root); var limit = getLimit(root); const startTime = getStartTime(root); const endTime = getEndTime(root); const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val(); // Start loading the next set of courses. // Fetch up to limit number of courses with at least one action event in the time filtering specified. // Courses without events will also be fetched, but hidden in case they have events in other timespans. return CourseRepository.getEnrolledCoursesWithEventsByTimelineClassification( COURSE_CLASSIFICATION, limit, offset, COURSE_SORT, searchValue, startTime, endTime ).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); const moreCoursesAvailable = result.morecoursesavailable; // Record the next offset if we want to request more courses. setOffset(root, nextOffset); // Load the events for these courses. var eventsPromise = loadEventsForCourses(courses, startTime, endTime, searchValue); // Render the courses in the DOM. var renderPromise = updateDisplayFromCourses(courses, root, midnight, daysOffset, daysLimit, append); return $.when(eventsPromise, renderPromise) .then(function(eventsByCourse) { if (hasReloadedEventsSince(root, startEventLoadingTime)) { // All of the events are being reloaded so ignore our results. return eventsByCourse; } if (courses.length > 0) { // Render the events in the correct course event list. courses.forEach(function(course) { const courseId = course.id; const containerSelector = '[data-region="course-events-container"][data-course-id="' + courseId + '"]'; const courseEventsContainer = root.find(containerSelector); const eventListRoot = courseEventsContainer.find(EventList.rootSelector); EventList.init(eventListRoot, additionalConfig); }); if (!moreCoursesAvailable) { // If no more courses with events matching the current filtering exist, hide the more courses button. hideMoreCoursesButton(root); } else { // If more courses exist with events matching the current filtering, show the more courses button. showMoreCoursesButton(root); } } else { // No more courses to load, hide the more courses button. hideMoreCoursesButton(root); // A zero offset means this was not loading "more courses", so we need to display the no results message. if (offset == 0) { showNoCoursesWithEventsMessage(root); } } return eventsByCourse; }); }).then(() => { return pendingPromise.resolve(); }).catch(Notification.exception); }; /** * 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, true) .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); // Only need to handle course loading if the user is actively enrolled in a course. if (!root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).length) { setEventReloadTime(root, Date.now()); if (root.hasClass('active')) { // Only load if this is active otherwise it will be lazy loaded later. loadMoreCourses(root); root.attr('data-seen', true); } 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) { setOffset(root, 0); showLoadingPlaceholder(root); hideNoCoursesWithEventsMessage(root); root.removeAttr('data-seen'); if (root.hasClass('active')) { shown(root); } }; /** * Begin loading the events unless we know there are no actively enrolled courses. * * @param {object} root The root element for the timeline courses view. */ var shown = function(root) { if (!root.attr('data-seen') && !root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).length) { loadMoreCourses(root); root.attr('data-seen', true); } }; return { init: init, reset: reset, shown: shown }; });