Commit 2263bbdf authored by Andrew Nicols's avatar Andrew Nicols
Browse files

Merge branch 'MDL-63058-master' of git://github.com/bmbrands/moodle

parents 1524c4d6 099049b6
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
......@@ -20,7 +20,7 @@
* @copyright 2018 Bas Brands <base@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['core/ajax'], function(Ajax) {
define(['core/ajax', 'core/notification'], function(Ajax, Notification) {
/**
* Retrieve a list of enrolled courses.
......@@ -47,7 +47,55 @@ define(['core/ajax'], function(Ajax) {
return promise;
};
/**
* Set the favourite state on a list of courses.
*
* Valid args are:
* Array courses list of course id numbers.
*
* @param {Object} args Arguments send to the webservice.
* @return {Promise} Resolve with warnings.
*/
var setFavouriteCourses = function(args) {
var request = {
methodname: 'core_course_set_favourite_courses',
args: args
};
var promise = Ajax.call([request])[0];
return promise;
};
/**
* Update the user preferences.
*
* @param {Object} args Arguments send to the webservice.
*
* Sample args:
* {
* preferences: [
* {
* type: 'block_example_user_sort_preference'
* value: 'title'
* }
* ]
* }
*/
var updateUserPreferences = function(args) {
var request = {
methodname: 'core_user_update_user_preferences',
args: args
};
Ajax.call([request])[0]
.fail(Notification.exception);
};
return {
getEnrolledCoursesByTimeline: getEnrolledCoursesByTimeline
getEnrolledCoursesByTimeline: getEnrolledCoursesByTimeline,
setFavouriteCourses: setFavouriteCourses,
updateUserPreferences: updateUserPreferences
};
});
......@@ -24,19 +24,31 @@
define(
[
'jquery',
'core/notification',
'block_myoverview/repository',
'core/paged_content_factory',
'core/custom_interaction_events',
'core/notification',
'core/templates',
],
function(
$,
Notification,
Repository,
PagedContentFactory,
CustomEvents,
Notification,
Templates
) {
var SELECTORS = {
ACTION_ADD_FAVOURITE: '[data-action="add-favourite"]',
ACTION_REMOVE_FAVOURITE: '[data-action="remove-favourite"]',
FAVOURITE_ICON: '[data-region="favourite-icon"]',
ICON_IS_FAVOURITE: '[data-region="is-favourite"]',
ICON_NOT_FAVOURITE: '[data-region="not-favourite"]',
PAGED_CONTENT_CONTAINER: '[data-region="page-container"]'
};
var TEMPLATES = {
COURSES_CARDS: 'block_myoverview/view-cards',
COURSES_LIST: 'block_myoverview/view-list',
......@@ -44,9 +56,9 @@ function(
NOCOURSES: 'block_myoverview/no-courses'
};
var NUMCOURSES_PERPAGE = [12, 24];
var NUMCOURSES_PERPAGE = [12, 24, 48];
var currentCourseList = [];
var loadedPages = [];
/**
* Get filter values from DOM.
......@@ -62,7 +74,7 @@ function(
return filters;
};
// We want the paged content controls below the paged content area
// 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,
......@@ -75,9 +87,10 @@ function(
* @param {object} filters The filters for this view.
* @param {int} limit The number of courses to show.
* @param {int} pageNumber The pagenumber to view.
* @return {promise} Resolved with an array of courses.
* @return {promise|Array} Resolved with an array of courses.
*/
var getMyCourses = function(filters, limit, pageNumber) {
return Repository.getEnrolledCoursesByTimeline({
offset: pageNumber * limit,
limit: limit,
......@@ -86,15 +99,178 @@ function(
});
};
/**
* Get the container element for the favourite icon.
*
* @param {Object} root The course overview container
* @param {Number} courseId Course id number
* @return {Object} The favourite icon container
*/
var getFavouriteIconContainer = function(root, courseId) {
return root.find(SELECTORS.FAVOURITE_ICON + '[data-course-id="' + courseId + '"]');
};
/**
* Get the paged content container element.
*
* @param {Object} root The course overview container
* @param {Number} index Rendered page index.
* @return {Object} The rendered paged container.
*/
var getPagedContentContainer = function(root, index) {
return root.find('[data-region="paged-content-page"][data-page="' + index + '"]');
};
/**
* Get the course id from a favourite element.
*
* @param {Object} root The favourite icon container element.
* @return {Number} Course id.
*/
var getFavouriteCourseId = function(root) {
return root.attr('data-course-id');
};
/**
* Hide the favourite icon.
*
* @param {Object} root The favourite icon container element.
* @param {Number} courseId Course id number.
*/
var hideFavouriteIcon = function(root, courseId) {
var iconContainer = getFavouriteIconContainer(root, courseId);
var isFavouriteIcon = iconContainer.find(SELECTORS.ICON_IS_FAVOURITE);
isFavouriteIcon.addClass('hidden');
isFavouriteIcon.attr('aria-hidden', true);
var notFavourteIcon = iconContainer.find(SELECTORS.ICON_NOT_FAVOURITE);
notFavourteIcon.removeClass('hidden');
notFavourteIcon.attr('aria-hidden', false);
};
/**
* Show the favourite icon.
*
* @param {Object} root The course overview container.
* @param {Number} courseId Course id number.
*/
var showFavouriteIcon = function(root, courseId) {
var iconContainer = getFavouriteIconContainer(root, courseId);
var isFavouriteIcon = iconContainer.find(SELECTORS.ICON_IS_FAVOURITE);
isFavouriteIcon.removeClass('hidden');
isFavouriteIcon.attr('aria-hidden', false);
var notFavourteIcon = iconContainer.find(SELECTORS.ICON_NOT_FAVOURITE);
notFavourteIcon.addClass('hidden');
notFavourteIcon.attr('aria-hidden', true);
};
/**
* Get the action menu item
*
* @param {Object} root root The course overview container
* @param {Number} courseId Course id.
* @return {Object} The add to favourite menu item.
*/
var getAddFavouriteMenuItem = function(root, courseId) {
return root.find('[data-action="add-favourite"][data-course-id="' + courseId + '"]');
};
/**
* Get the action menu item
*
* @param {Object} root root The course overview container
* @param {Number} courseId Course id.
* @return {Object} The remove from favourites menu item.
*/
var getRemoveFavouriteMenuItem = function(root, courseId) {
return root.find('[data-action="remove-favourite"][data-course-id="' + courseId + '"]');
};
/**
* Add course to favourites
*
* @param {Object} root The course overview container
* @param {Number} courseId Course id number
*/
var addToFavourites = function(root, courseId) {
var removeAction = getRemoveFavouriteMenuItem(root, courseId);
var addAction = getAddFavouriteMenuItem(root, courseId);
setCourseFavouriteState(courseId, true).then(function(success) {
if (success) {
removeAction.removeClass('hidden');
addAction.addClass('hidden');
showFavouriteIcon(root, courseId);
} else {
Notification.alert('Starring course failed', 'Could not change favourite state');
}
return;
}).catch(Notification.exception);
};
/**
* Remove course from favourites
*
* @param {Object} root The course overview container
* @param {Number} courseId Course id number
*/
var removeFromFavourites = function(root, courseId) {
var removeAction = getRemoveFavouriteMenuItem(root, courseId);
var addAction = getAddFavouriteMenuItem(root, courseId);
setCourseFavouriteState(courseId, false).then(function(success) {
if (success) {
removeAction.addClass('hidden');
addAction.removeClass('hidden');
hideFavouriteIcon(root, courseId);
} else {
Notification.alert('Starring course failed', 'Could not change favourite state');
}
return;
}).catch(Notification.exception);
};
/**
* Set the courses favourite status and push to repository
*
* @param {Number} courseId Course id to favourite.
* @param {Bool} status new favourite status.
* @return {Promise} Repository promise.
*/
var setCourseFavouriteState = function(courseId, status) {
return Repository.setFavouriteCourses({
courses: [
{
'id': courseId,
'favourite': status
}
]
}).then(function(result) {
if (result.warnings.length == 0) {
loadedPages.forEach(function(courseList) {
courseList.courses.forEach(function(course, index) {
if (course.id == courseId) {
courseList.courses[index].isfavourite = status;
}
});
});
return true;
} else {
return false;
}
}).catch(Notification.exception);
};
/**
* Render the dashboard courses.
*
* @param {object} root The root element for the courses view.
* @param {array} coursesData containing array of returned courses.
* @param {object} filters The filters for this view.
* @return {promise} jQuery promise resolved after rendering is complete.
*/
var renderCourses = function(root, coursesData, filters) {
var renderCourses = function(root, coursesData) {
var filters = getFilterValues(root);
var currentTemplate = '';
if (filters.display == 'cards') {
......@@ -127,6 +303,11 @@ function(
root = $(root);
if (!root.attr('data-init')) {
registerEventListeners(root);
root.attr('data-init', true);
}
var filters = getFilterValues(root);
var pagedContentPromise = PagedContentFactory.createWithLimit(
......@@ -135,6 +316,7 @@ function(
var promises = [];
pagesData.forEach(function(pageData) {
var currentPage = pageData.pageNumber;
var pageNumber = pageData.pageNumber - 1;
var pagePromise = getMyCourses(
......@@ -145,8 +327,8 @@ function(
if (coursesData.courses.length < pageData.limit) {
actions.allItemsLoaded(pageData.pageNumber);
}
currentCourseList = coursesData;
return renderCourses(root, coursesData, filters);
loadedPages[currentPage] = coursesData;
return renderCourses(root, coursesData);
})
.catch(Notification.exception);
......@@ -163,6 +345,35 @@ function(
}).catch(Notification.exception);
};
/**
* Listen to, and handle events for the myoverview block.
*
* @param {Object} root The myoverview block container element.
*/
var registerEventListeners = function(root) {
CustomEvents.define(root, [
CustomEvents.events.activate
]);
root.on(CustomEvents.events.activate, SELECTORS.ACTION_ADD_FAVOURITE, function(e, data) {
var favourite = $(e.target).closest(SELECTORS.ACTION_ADD_FAVOURITE);
var courseId = getFavouriteCourseId(favourite);
addToFavourites(root, courseId);
data.originalEvent.preventDefault();
});
root.on(CustomEvents.events.activate, SELECTORS.ACTION_REMOVE_FAVOURITE, function(e, data) {
var favourite = $(e.target).closest(SELECTORS.ACTION_REMOVE_FAVOURITE);
var courseId = getFavouriteCourseId(favourite);
removeFromFavourites(root, courseId);
data.originalEvent.preventDefault();
});
root.on(CustomEvents.events.activate, SELECTORS.FAVOURITE_ICON, function(e, data) {
data.originalEvent.preventDefault();
});
};
/**
* Reset the courses views to their original
* state on first page load.
......@@ -170,15 +381,21 @@ function(
* 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.
* @param {object} content The content element for the timeline view.
* @param {Object} root The root element for the timeline view.
* @param {Object} content The content element for the timeline view.
*/
var reset = function(root, content) {
var filters = getFilterValues(root);
renderCourses(root, currentCourseList, filters)
.then(function(html, js) {
return Templates.replaceNodeContents(content, html, js);
}).catch(Notification.exception);
if (loadedPages.length > 0) {
loadedPages.forEach(function(courseList, index) {
var pagedContentPage = getPagedContentContainer(root, index);
renderCourses(root, courseList).then(function(html, js) {
return Templates.replaceNodeContents(pagedContentPage, html, js);
}).catch(Notification.exception);
});
} else {
init(root, content);
}
};
return {
......
......@@ -25,11 +25,13 @@ define(
[
'jquery',
'core/custom_interaction_events',
'block_myoverview/repository',
'block_myoverview/view'
],
function(
$,
CustomEvents,
Repository,
View
) {
......@@ -39,6 +41,32 @@ function(
DISPLAY_OPTION: '[data-display-option]'
};
/**
* Update the user preference for the block.
*
* @param {String} filter The type of filter: display/sort/grouping.
* @param {String} value The current preferred value.
*/
var updatePreferences = function(filter, value) {
var type = null;
if (filter == 'display') {
type = 'block_myoverview_user_view_preference';
} else if (filter == 'sort') {
type = 'block_myoverview_user_sort_preference';
} else {
type = 'block_myoverview_user_grouping_preference';
}
Repository.updateUserPreferences({
preferences: [
{
type: type,
value: value
}
]
});
};
/**
* Event listener for the Display filter (cards, list).
*
......@@ -62,8 +90,14 @@ function(
return;
}
var attributename = 'data-' + option.attr('data-filter');
viewRoot.attr(attributename, option.attr('data-value'));
var filter = option.attr('data-filter');
var attributename = 'data-' + filter;
var value = option.attr('data-value');
var pref = option.attr('data-pref');
viewRoot.attr(attributename, value);
updatePreferences(filter, pref);
// Reset the views.
View.init(viewRoot, viewContent);
......@@ -83,7 +117,12 @@ function(
return;
}
viewRoot.attr('data-display', option.attr('data-value'));
var filter = option.attr('data-display-option');
var value = option.attr('data-value');
var pref = option.attr('data-pref');
updatePreferences(filter, pref);
viewRoot.attr('data-display', value);
View.reset(viewRoot, viewContent);
data.originalEvent.preventDefault();
}
......
......@@ -49,8 +49,11 @@ class block_myoverview extends block_base {
if (isset($this->content)) {
return $this->content;
}
$group = get_user_preferences('block_myoverview_user_grouping_preference');
$sort = get_user_preferences('block_myoverview_user_sort_preference');
$view = get_user_preferences('block_myoverview_user_view_preference');
$renderable = new \block_myoverview\output\main();
$renderable = new \block_myoverview\output\main($group, $sort, $view);
$renderer = $this->page->get_renderer('block_myoverview');
$this->content = new stdClass();
......
......@@ -28,7 +28,7 @@ use renderable;
use renderer_base;
use templatable;
require_once($CFG->libdir . '/completionlib.php');
require_once($CFG->dirroot . '/blocks/myoverview/lib.php');
/**
* Class containing data for my overview block.
......@@ -37,18 +37,75 @@ require_once($CFG->libdir . '/completionlib.php');
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class main implements renderable, templatable {
/**
* Store the grouping preference
*
* @var string String matching the grouping constants defined in myoverview/lib.php
*/
private $grouping;
/**
* Store the sort preference
*
* @var string String matching the sort constants defined in myoverview/lib.php
*/
private $sort;
/**
* Store the view preference
*
* @var string String matching the view/display constants defined in myoverview/lib.php
*/
private $view;
/**
* main constructor.
* Initialize the user preferences
*
* @param string $grouping Grouping user preference
* @param string $sort Sort user preference
* @param string $view Display user preference
*/
public function __construct($grouping, $sort, $view) {
$this->grouping = $grouping ? $grouping : BLOCK_MYOVERVIEW_GROUPING_ALL;
$this->sort = $sort ? $sort : BLOCK_MYOVERVIEW_SORTING_TITLE;
$this->view = $view ? $view : BLOCK_MYOVERVIEW_VIEW_CARD;
}
/**
* Get the user preferences as an array to figure out what has been selected
*
* @return array $preferences Array with the pref as key and value set to true
*/
public function get_preferences_as_booleans() {
$preferences = [];
$preferences[$this->view] = true;
$preferences[$this->sort] = true;
$preferences[$this->grouping] = true;
return $preferences;
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param \renderer_base $output
* @return stdClass
* @return array Context variables for the template
*/
public function export_for_template(renderer_base $output) {
$nocoursesurl = $output->image_url('courses', 'block_myoverview')->out();
return (object) [
'nocoursesimg' => $nocoursesurl
$defaultvariables = [
'nocoursesimg' => $nocoursesurl,
'grouping' => $this->grouping,
'sort' => $this->sort == BLOCK_MYOVERVIEW_SORTING_TITLE ? 'fullname' : 'ul.timeaccess desc',
'view' => $this->view
];
$preferences = $this->get_preferences_as_booleans();
return array_merge($defaultvariables, $preferences);
}
}
}
\ No newline at end of file
......@@ -24,6 +24,9 @@
namespace block_myoverview\privacy;
use core_privacy\local\request\user_preference_provider;
use core_privacy\local\metadata\collection;
defined('MOODLE_INTERNAL') || die();
/**
......@@ -32,15 +35,48 @@ 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\null_provider {
class provider implements \core_privacy\local\metadata\provider, user_preference_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
* Returns meta-data information about the myoverview block.
*
* @return string
* @param \core_privacy\local\metadata\collection $collection A collection of meta-data.
* @return \core_privacy\local\metadata\collection Return the collection of meta-data.