Merge branch 'MDL-63457-master' of git://github.com/peterRd/moodle

This commit is contained in:
Andrew Nicols 2018-11-01 15:03:34 +08:00
commit f3d077d0a7
32 changed files with 843 additions and 99 deletions

View File

@ -1 +1 @@
define(["jquery","block_myoverview/view","block_myoverview/view_nav"],function(a,b,c){var d={COURSES_VIEW:'[data-region="courses-view"]',COURSES_VIEW_CONTENT:'[data-region="course-view-content"]'},e=function(e){e=a(e);var f=e.find(d.COURSES_VIEW),g=e.find(d.COURSES_VIEW_CONTENT);c.init(e,f,g),b.init(f,g)};return{init:e}});
define(["jquery","block_myoverview/view","block_myoverview/view_nav"],function(a,b,c){var d=function(d){d=a(d),c.init(d),b.init(d)};return{init:d}});

View File

@ -0,0 +1 @@
define([],function(){return{courseView:{region:'[data-region="courses-view"]',regionContent:'[data-region="course-view-content"]'}}});

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
define(["jquery","core/custom_interaction_events","block_myoverview/repository","block_myoverview/view"],function(a,b,c,d){var e={FILTERS:'[data-region="filter"]',FILTER_OPTION:"[data-filter]",DISPLAY_OPTION:"[data-display-option]"},f=function(a,b){var d=null;d="display"==a?"block_myoverview_user_view_preference":"sort"==a?"block_myoverview_user_sort_preference":"block_myoverview_user_grouping_preference",c.updateUserPreferences({preferences:[{type:d,value:b}]})},g=function(c,g,h){var i=c.find(e.FILTERS);b.define(i,[b.events.activate]),i.on(b.events.activate,e.FILTER_OPTION,function(b,c){var e=a(b.target);if(!e.hasClass("active")){var i=e.attr("data-filter"),j="data-"+i,k=e.attr("data-value"),l=e.attr("data-pref");g.attr(j,k),f(i,l),d.init(g,h),c.originalEvent.preventDefault()}}),b.define(i,[b.events.activate]),i.on(b.events.activate,e.DISPLAY_OPTION,function(b,c){var e=a(b.target);if(!e.hasClass("active")){var i=e.attr("data-display-option"),j=e.attr("data-value"),k=e.attr("data-pref");f(i,k),g.attr("data-display",j),d.reset(g,h),c.originalEvent.preventDefault()}})},h=function(b,c,d){b=a(b),g(b,c,d)};return{init:h}});
define(["jquery","core/custom_interaction_events","block_myoverview/repository","block_myoverview/view","block_myoverview/selectors"],function(a,b,c,d,e){var f={FILTERS:'[data-region="filter"]',FILTER_OPTION:"[data-filter]",DISPLAY_OPTION:"[data-display-option]"},g=function(a,b){var d=null;d="display"==a?"block_myoverview_user_view_preference":"sort"==a?"block_myoverview_user_sort_preference":"block_myoverview_user_grouping_preference",c.updateUserPreferences({preferences:[{type:d,value:b}]})},h=function(c){var h=c.find(f.FILTERS);b.define(h,[b.events.activate]),h.on(b.events.activate,f.FILTER_OPTION,function(b,f){var h=a(b.target);if(!h.hasClass("active")){var i=h.attr("data-filter"),j=h.attr("data-pref");c.find(e.courseView.region).attr("data-"+i,h.attr("data-value")),g(i,j),d.init(c),f.originalEvent.preventDefault()}}),b.define(h,[b.events.activate]),h.on(b.events.activate,f.DISPLAY_OPTION,function(b,f){var h=a(b.target);if(!h.hasClass("active")){var i=h.attr("data-display-option"),j=h.attr("data-pref");c.find(e.courseView.region).attr("data-display",h.attr("data-value")),g(i,j),d.reset(c),f.originalEvent.preventDefault()}})},i=function(b){b=a(b),h(b)};return{init:i}});

View File

@ -32,12 +32,6 @@ function(
View,
ViewNav
) {
var SELECTORS = {
COURSES_VIEW: '[data-region="courses-view"]',
COURSES_VIEW_CONTENT: '[data-region="course-view-content"]'
};
/**
* Initialise all of the modules for the overview block.
*
@ -45,12 +39,10 @@ function(
*/
var init = function(root) {
root = $(root);
var coursesViewRoot = root.find(SELECTORS.COURSES_VIEW);
var coursesViewContent = root.find(SELECTORS.COURSES_VIEW_CONTENT);
// Initialise the course navigation elements.
ViewNav.init(root, coursesViewRoot, coursesViewContent);
ViewNav.init(root);
// Initialise the courses view modules.
View.init(coursesViewRoot, coursesViewContent);
View.init(root);
};
return {

View File

@ -0,0 +1,31 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Javascript to initialise the selectors for the myoverview block.
*
* @package block_myoverview
* @copyright 2018 Peter Dias <peter@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([], function() {
return {
courseView: {
region: '[data-region="courses-view"]',
regionContent: '[data-region="course-view-content"]'
}
};
});

View File

@ -30,7 +30,8 @@ define(
'core/custom_interaction_events',
'core/notification',
'core/templates',
'core_course/events'
'core_course/events',
'block_myoverview/selectors'
],
function(
$,
@ -40,10 +41,14 @@ function(
CustomEvents,
Notification,
Templates,
CourseEvents
CourseEvents,
Selectors
) {
var SELECTORS = {
COURSE_REGION: '[data-region="course-view-content"]',
ACTION_HIDE_COURSE: '[data-action="hide-course"]',
ACTION_SHOW_COURSE: '[data-action="show-course"]',
ACTION_ADD_FAVOURITE: '[data-action="add-favourite"]',
ACTION_REMOVE_FAVOURITE: '[data-action="remove-favourite"]',
FAVOURITE_ICON: '[data-region="favourite-icon"]',
@ -64,6 +69,10 @@ function(
var loadedPages = [];
var courseOffset = 0;
var lastPage = 0;
/**
* Get filter values from DOM.
*
@ -71,11 +80,12 @@ function(
* @return {filters} Set filters.
*/
var getFilterValues = function(root) {
var filters = {};
filters.display = root.attr('data-display');
filters.grouping = root.attr('data-grouping');
filters.sort = root.attr('data-sort');
return filters;
var courseRegion = root.find(Selectors.courseView.region);
return {
display: courseRegion.attr('data-display'),
grouping: courseRegion.attr('data-grouping'),
sort: courseRegion.attr('data-sort')
};
};
// We want the paged content controls below the paged content area.
@ -90,13 +100,12 @@ 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.
*/
var getMyCourses = function(filters, limit, pageNumber) {
var getMyCourses = function(filters, limit) {
return Repository.getEnrolledCoursesByTimeline({
offset: pageNumber * limit,
offset: courseOffset,
limit: limit,
classification: filters.grouping,
sort: filters.sort
@ -131,7 +140,7 @@ function(
* @param {Object} root The favourite icon container element.
* @return {Number} Course id.
*/
var getFavouriteCourseId = function(root) {
var getCourseId = function(root) {
return root.attr('data-course-id');
};
@ -235,6 +244,61 @@ function(
}).catch(Notification.exception);
};
/**
* Reset the loadedPages dataset to take into account the hidden element
*
* @param {Object} root The course overview container
* @param {Object} target The course that you want to hide
*/
var hideElement = function(root, target) {
var id = getCourseId(target);
var pagingBar = root.find('[data-region="paging-bar"]');
var jumpto = parseInt(pagingBar.attr('data-active-page-number'));
// Get a reduced dataset for the current page.
var courseList = loadedPages[jumpto];
var reducedCourse = courseList.courses.reduce(function(accumulator, current) {
if (id != current.id) {
accumulator.push(current);
}
return accumulator;
}, []);
// Get the next page's data if loaded and pop the first element from it
if (loadedPages[jumpto + 1] != undefined) {
var newElement = loadedPages[jumpto + 1].courses.slice(0, 1);
loadedPages[jumpto + 1].courses = loadedPages[jumpto + 1].courses.slice(1);
reducedCourse = $.merge(reducedCourse, newElement);
}
// Check if the next page is the last page and if it still has data associated to it
if (lastPage == jumpto + 1 && loadedPages[jumpto + 1].courses.length == 0) {
var pagedContentContainer = root.find('[data-region="paged-content-container"]');
PagedContentFactory.resetLastPageNumber($(pagedContentContainer).attr('id'), jumpto);
}
loadedPages[jumpto].courses = reducedCourse;
// Reduce the course offset
courseOffset--;
// Render the paged content for the current
var pagedContentPage = getPagedContentContainer(root, jumpto);
renderCourses(root, loadedPages[jumpto]).then(function(html, js) {
return Templates.replaceNodeContents(pagedContentPage, html, js);
}).catch(Notification.exception);
// Delete subsequent pages in order to trigger the callback
loadedPages.forEach(function(courseList, index) {
if (index > jumpto) {
var page = getPagedContentContainer(root, index);
page.remove();
}
});
};
/**
* Set the courses favourite status and push to repository
*
@ -292,7 +356,7 @@ function(
courses: coursesData.courses
});
} else {
var nocoursesimg = root.attr('data-nocoursesimg');
var nocoursesimg = root.find(Selectors.courseView.region).attr('data-nocoursesimg');
return Templates.render(TEMPLATES.NOCOURSES, {
nocoursesimg: nocoursesimg
});
@ -300,20 +364,12 @@ function(
};
/**
* Intialise the courses list and cards views on page load.
* Intialise the paged list and cards views on page load.
*
* @param {object} root The root element for the courses view.
* @param {object} content The content element for the courses view.
*/
var init = function(root, content) {
root = $(root);
if (!root.attr('data-init')) {
registerEventListeners(root);
root.attr('data-init', true);
}
var initializePagedContent = function(root) {
var filters = getFilterValues(root);
var pagedContentPromise = PagedContentFactory.createWithLimit(
@ -323,18 +379,65 @@ function(
pagesData.forEach(function(pageData) {
var currentPage = pageData.pageNumber;
var pageNumber = pageData.pageNumber - 1;
var limit = pageData.limit;
if (lastPage == currentPage) {
// If we are on the last page and have it's data then load it from cache
actions.allItemsLoaded(lastPage);
promises.push(renderCourses(root, loadedPages[currentPage]));
return;
}
// Get 2 pages worth of data as we will need it for the hidden functionality.
if (loadedPages[currentPage + 1] == undefined) {
if (loadedPages[currentPage] == undefined) {
limit *= 2;
}
}
var pagePromise = getMyCourses(
filters,
pageData.limit,
pageNumber
limit
).then(function(coursesData) {
if (coursesData.courses.length < pageData.limit) {
actions.allItemsLoaded(pageData.pageNumber);
var courses = coursesData.courses;
var nextPageStart = 0;
var pageCourses = [];
// If current page's data is loaded make sure we max it to page limit
if (loadedPages[currentPage] != undefined) {
pageCourses = loadedPages[currentPage].courses;
var currentPageLength = pageCourses.length;
if (currentPageLength < pageData.limit) {
nextPageStart = pageData.limit - currentPageLength;
pageCourses = $.merge(loadedPages[currentPage].courses, courses.slice(0, nextPageStart));
}
} else {
nextPageStart = pageData.limit;
pageCourses = courses.slice(0, pageData.limit);
}
loadedPages[currentPage] = coursesData;
return renderCourses(root, coursesData);
// Finished setting up the current page
loadedPages[currentPage] = {
courses: pageCourses
};
// Set up the next page
var remainingCourses = courses.slice(nextPageStart, courses.length);
loadedPages[currentPage + 1] = {
courses: remainingCourses
};
// Set the last page to either the current or next page
if (loadedPages[currentPage].courses.length < pageData.limit) {
lastPage = currentPage;
actions.allItemsLoaded(currentPage);
} else if (loadedPages[currentPage + 1] != undefined
&& loadedPages[currentPage + 1].courses.length < pageData.limit) {
lastPage = currentPage + 1;
}
courseOffset = coursesData.nextoffset;
return renderCourses(root, loadedPages[currentPage]);
})
.catch(Notification.exception);
@ -347,7 +450,7 @@ function(
);
pagedContentPromise.then(function(html, js) {
return Templates.replaceNodeContents(content, html, js);
return Templates.replaceNodeContents(root.find(Selectors.courseView.region), html, js);
}).catch(Notification.exception);
};
@ -363,14 +466,14 @@ function(
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);
var courseId = getCourseId(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);
var courseId = getCourseId(favourite);
removeFromFavourites(root, courseId);
data.originalEvent.preventDefault();
});
@ -378,20 +481,75 @@ function(
root.on(CustomEvents.events.activate, SELECTORS.FAVOURITE_ICON, function(e, data) {
data.originalEvent.preventDefault();
});
root.on(CustomEvents.events.activate, SELECTORS.ACTION_HIDE_COURSE, function(e, data) {
var target = $(e.target).closest(SELECTORS.ACTION_HIDE_COURSE);
var id = getCourseId(target);
var request = {
preferences: [
{
type: 'block_myoverview_hidden_course_' + id,
value: true
}
]
};
Repository.updateUserPreferences(request);
hideElement(root, target);
data.originalEvent.preventDefault();
});
root.on(CustomEvents.events.activate, SELECTORS.ACTION_SHOW_COURSE, function(e, data) {
var target = $(e.target).closest(SELECTORS.ACTION_SHOW_COURSE);
var id = getCourseId(target);
var request = {
preferences: [
{
type: 'block_myoverview_hidden_course_' + id,
value: null
}
]
};
Repository.updateUserPreferences(request);
hideElement(root, target);
data.originalEvent.preventDefault();
});
};
/**
* Intialise the courses list and cards views on page load.
*
* @param {object} root The root element for the courses view.
*/
var init = function(root) {
root = $(root);
loadedPages = [];
lastPage = 0;
courseOffset = 0;
if (!root.attr('data-init')) {
registerEventListeners(root);
root.attr('data-init', true);
}
initializePagedContent(root);
};
/**
* Reset the courses views to their original
* state on first page load.
* state on first page load.courseOffset
*
* 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.
*/
var reset = function(root, content) {
var reset = function(root) {
if (loadedPages.length > 0) {
loadedPages.forEach(function(courseList, index) {
var pagedContentPage = getPagedContentContainer(root, index);
@ -400,7 +558,7 @@ function(
}).catch(Notification.exception);
});
} else {
init(root, content);
init(root);
}
};

View File

@ -26,13 +26,15 @@ define(
'jquery',
'core/custom_interaction_events',
'block_myoverview/repository',
'block_myoverview/view'
'block_myoverview/view',
'block_myoverview/selectors'
],
function(
$,
CustomEvents,
Repository,
View
View,
Selectors
) {
var SELECTORS = {
@ -71,10 +73,8 @@ function(
* Event listener for the Display filter (cards, list).
*
* @param {object} root The root element for the overview block
* @param {object} viewRoot The root element for displaying courses.
* @param {object} viewContent content The content element for the courses view.
*/
var registerSelector = function(root, viewRoot, viewContent) {
var registerSelector = function(root) {
var Selector = root.find(SELECTORS.FILTERS);
@ -91,16 +91,13 @@ function(
}
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);
root.find(Selectors.courseView.region).attr('data-' + filter, option.attr('data-value'));
updatePreferences(filter, pref);
// Reset the views.
View.init(viewRoot, viewContent);
View.init(root);
data.originalEvent.preventDefault();
}
@ -118,12 +115,11 @@ function(
}
var filter = option.attr('data-display-option');
var value = option.attr('data-value');
var pref = option.attr('data-pref');
root.find(Selectors.courseView.region).attr('data-display', option.attr('data-value'));
updatePreferences(filter, pref);
viewRoot.attr('data-display', value);
View.reset(viewRoot, viewContent);
View.reset(root);
data.originalEvent.preventDefault();
}
);
@ -134,12 +130,10 @@ function(
* the navigation elements.
*
* @param {object} root The root element for the myoverview block
* @param {object} viewRoot The root element for the myoverview block
* @param {object} viewContent The content element for the myoverview block
*/
var init = function(root, viewRoot, viewContent) {
var init = function(root) {
root = $(root);
registerSelector(root, viewRoot, viewContent);
registerSelector(root);
};
return {

View File

@ -26,6 +26,7 @@ namespace block_myoverview\privacy;
use core_privacy\local\request\user_preference_provider;
use core_privacy\local\metadata\collection;
use \core_privacy\local\request\writer;
defined('MOODLE_INTERNAL') || die();
@ -58,14 +59,14 @@ class provider implements \core_privacy\local\metadata\provider, user_preference
public static function export_user_preferences(int $userid) {
$preference = get_user_preferences('block_myoverview_user_sort_preference', null, $userid);
if (isset($preference)) {
\core_privacy\local\request\writer::export_user_preference('block_myoverview',
writer::export_user_preference('block_myoverview',
'block_myoverview_user_sort_preference', get_string($preference, 'block_myoverview'),
get_string('privacy:metadata:overviewsortpreference', 'block_myoverview'));
}
$preference = get_user_preferences('block_myoverview_user_view_preference', null, $userid);
if (isset($preference)) {
\core_privacy\local\request\writer::export_user_preference('block_myoverview',
writer::export_user_preference('block_myoverview',
'block_myoverview_user_view_preference',
get_string($preference, 'block_myoverview'),
get_string('privacy:metadata:overviewviewpreference', 'block_myoverview'));
@ -73,10 +74,25 @@ class provider implements \core_privacy\local\metadata\provider, user_preference
$preference = get_user_preferences('block_myoverview_user_grouping_preference', null, $userid);
if (isset($preference)) {
\core_privacy\local\request\writer::export_user_preference('block_myoverview',
writer::export_user_preference('block_myoverview',
'block_myoverview_user_grouping_preference',
get_string($preference, 'block_myoverview'),
get_string('privacy:metadata:overviewgroupingpreference', 'block_myoverview'));
}
$preferences = get_user_preferences(null, null, $userid);
foreach ($preferences as $name => $value) {
if ((substr($name, 0, 30) == 'block_myoverview_hidden_course')) {
writer::export_user_preference(
'block_myoverview',
$name,
$value,
get_string('privacy:request:preference:set', 'block_myoverview', (object) [
'name' => $name,
'value' => $value,
])
);
}
}
}
}

View File

@ -45,6 +45,7 @@ $string['aria:summary'] = 'Switch to summary view';
$string['aria:sortingdropdown'] = 'Sorting dropdown';
$string['card'] = 'Card';
$string['cards'] = 'Cards';
$string['courseprogress'] = 'Course progress:';
$string['complete'] = 'Complete';
$string['favourites'] = 'Starred';
$string['future'] = 'Future';
@ -62,6 +63,14 @@ $string['privacy:metadata:overviewgroupingpreference'] = 'The Course overview bl
$string['removefromfavourites'] = 'Unstar this course';
$string['summary'] = 'Summary';
$string['title'] = 'Title';
$string['aria:hidecourse'] = 'Hide {$a} from view';
$string['aria:showcourse'] = 'Show {$a} in view';
$string['aria:hiddencourses'] = 'Show hidden courses';
$string['hidden'] = 'Hidden courses';
$string['hidecourse'] = 'Hide from view';
$string['hiddencourses'] = 'Hidden';
$string['show'] = 'Show this course';
$string['privacy:request:preference:set'] = 'The value of the setting \'{$a->name}\' was \'{$a->value}\'';
// Deprecated since Moodle 3.6.
$string['defaulttab'] = 'Default tab';
@ -79,4 +88,4 @@ $string['sortbydates'] = 'Sort by dates';
$string['timeline'] = 'Timeline';
$string['viewcoursename'] = 'View course {$a}';
$string['privacy:metadata:overviewlasttab'] = 'This stores the last tab selected by the user on the overview block.';
$string['viewcourse'] = 'View course';

View File

@ -32,6 +32,7 @@ define('BLOCK_MYOVERVIEW_GROUPING_INPROGRESS', 'inprogress');
define('BLOCK_MYOVERVIEW_GROUPING_FUTURE', 'future');
define('BLOCK_MYOVERVIEW_GROUPING_PAST', 'past');
define('BLOCK_MYOVERVIEW_GROUPING_FAVOURITES', 'favourites');
define('BLOCK_MYOVERVIEW_GROUPING_HIDDEN', 'hidden');
/**
* Constants for the user preferences sorting options
@ -62,7 +63,8 @@ function block_myoverview_user_preferences() {
BLOCK_MYOVERVIEW_GROUPING_INPROGRESS,
BLOCK_MYOVERVIEW_GROUPING_FUTURE,
BLOCK_MYOVERVIEW_GROUPING_PAST,
BLOCK_MYOVERVIEW_GROUPING_FAVOURITES
BLOCK_MYOVERVIEW_GROUPING_FAVOURITES,
BLOCK_MYOVERVIEW_GROUPING_HIDDEN
)
);
$preferences['block_myoverview_user_sort_preference'] = array(
@ -84,5 +86,14 @@ function block_myoverview_user_preferences() {
BLOCK_MYOVERVIEW_VIEW_SUMMARY
)
);
$preferences['/^block_myoverview_hidden_course_(\d)+$/'] = array(
'isregex' => true,
'choices' => array(0, 1),
'type' => PARAM_INT,
'null' => NULL_NOT_ALLOWED,
'default' => 'none'
);
return $preferences;
}
}

View File

@ -57,5 +57,27 @@
{{#str}} aria:removefromfavourites, block_myoverview {{/str}} {{{fullname}}}
</div>
</a>
<a class="dropdown-item {{^hidden}}hidden{{/hidden}}" href="#"
data-action="show-course"
data-course-id="{{id}}"
aria-controls="favorite-icon-{{ id }}"
>
{{#pix}} i/show, core, {{#str}} hidden, block_myoverview {{/str}} {{/pix}}
{{#str}} show, block_myoverview {{/str}}
<div class="sr-only">
{{#str}} aria:showcourse, block_myoverview, {{fullname}} {{/str}}
</div>
</a>
<a class="dropdown-item {{#hidden}}hidden{{/hidden}}" href="#"
data-action="hide-course"
data-course-id="{{id}}"
aria-controls="favorite-icon-{{ id }}"
>
{{#pix}} i/hide, core, {{#str}} hidden, block_myoverview {{/str}} {{/pix}}
{{#str}} hidecourse, block_myoverview {{/str}}
<div class="sr-only">
{{#str}} aria:hidecourse, block_myoverview, {{fullname}} {{/str}}
</div>
</a>
</div>
</div>

View File

@ -32,6 +32,7 @@
data-display="{{view}}"
data-grouping="{{grouping}}"
data-sort="{{sort}}"
data-prev-display="{{view}}"
data-nocoursesimg="{{nocoursesimg}}">
<div data-region="course-view-content">
<div data-region="courses-loading-placeholder">

View File

@ -36,6 +36,7 @@
{{#future}}{{#str}} future, block_myoverview {{/str}}{{/future}}
{{#past}}{{#str}} past, block_myoverview {{/str}}{{/past}}
{{#favourites}}{{#str}} favourites, block_myoverview {{/str}}{{/favourites}}
{{#hidden}}{{#str}} hiddencourses, block_myoverview {{/str}}{{/hidden}}
</span>
</button>
<ul class="dropdown-menu" data-show-active-item data-active-item-text aria-labelledby="groupingdropdown">
@ -64,5 +65,10 @@
{{#str}} favourites, block_myoverview {{/str}}
</a>
</li>
<li>
<a class="dropdown-item {{#hidden}}active{{/hidden}}" href="#" data-filter="grouping" data-value="hidden" data-pref="hidden" aria-label="{{#str}} aria:hiddencourses, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
{{#str}} hiddencourses, block_myoverview {{/str}}
</a>
</li>
</ul>
</div>

View File

@ -36,7 +36,9 @@
<div class="card-deck dashboard-card-deck" role="list">
{{#courses}}
<div class="card dashboard-card" role="listitem">
<div class="card dashboard-card" role="listitem"
data-region="course-content"
data-course-id="{{{id}}}">
<a href="{{viewurl}}" tabindex="-1">
<div class="card-img dashboard-card-img" style='background-image: url("{{{courseimage}}}");'>
<span class="sr-only">{{#str}}aria:courseimage, core_course{{/str}}</span>

View File

@ -36,7 +36,9 @@
<ul class="list-group">
{{#courses}}
<li class="list-group-item course-listitem">
<li class="list-group-item course-listitem"
data-region="course-content"
data-course-id="{{{id}}}">
<div class="row-fluid">
<div class="{{#hasprogress}}col-6 span6{{/hasprogress}}{{^hasprogress}}col-11 span11{{/hasprogress}} p-l-0">
<div class="d-flex align-items-center">

View File

@ -36,7 +36,9 @@
}}
<div role="list">
{{#courses}}
<div class="course-summaryitem m-b-1 p-2" role="listitem">
<div class="course-summaryitem m-b-1 p-2" role="listitem"
data-region="course-content"
data-course-id="{{{id}}}">
<div class="row-fluid d-flex">
<a href="{{viewurl}}" class="col-sm-4 col-xl-3 span4 position-relative" tabindex="-1">
<div class="position-absolute">

View File

@ -157,4 +157,46 @@ Feature: The my overview block allows users to easily access their courses
And I click on "Last accessed" "link" in the "Course overview" "block"
And I reload the page
Then I should see "Last accessed" in the "Course overview" "block"
And "[data-sort='ul.timeaccess desc']" "css_element" in the "Course overview" "block" should be visible
And "[data-sort='ul.timeaccess desc']" "css_element" in the "Course overview" "block" should be visible
Scenario: View inprogress courses with hide persistent functionality
Given I log in as "student1"
And I click on "All" "button" in the "Course overview" "block"
When I click on "In progress" "link" in the "Course overview" "block"
And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
And I click on "Hide from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
And I reload the page
Then I should see "Course 3" in the "Course overview" "block"
Then I should see "Course 4" in the "Course overview" "block"
And I should not see "Course 2" in the "Course overview" "block"
And I should not see "Course 1" in the "Course overview" "block"
And I should not see "Course 5" in the "Course overview" "block"
And I log out
Scenario: View past courses with hide persistent functionality
Given I log in as "student1"
And I click on "All" "button" in the "Course overview" "block"
When I click on "Past" "link" in the "Course overview" "block"
And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 1')]" "xpath_element"
And I click on "Hide from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 1')]" "xpath_element"
And I reload the page
Then I should not 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 should not see "Course 4" in the "Course overview" "block"
And I should not see "Course 5" in the "Course overview" "block"
And I log out
Scenario: View future courses with hide persistent functionality
Given I log in as "student1"
And I click on "All" "button" in the "Course overview" "block"
When I click on "Future" "link" in the "Course overview" "block"
And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 5')]" "xpath_element"
And I click on "Hide from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 5')]" "xpath_element"
And I reload the page
Then I should not see "Course 5" in the "Course overview" "block"
And I should not 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 should not see "Course 4" in the "Course overview" "block"
And I log out

View File

@ -28,11 +28,11 @@ Feature: The my overview block allows users to favourite their courses
When I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
And I click on "Star this course" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
And I reload the page
Then "//div[@role='listitem' and contains(.,'Course 2')]//span[@data-region='is-favourite' and @aria-hidden='false']" "xpath_element" should exist
And "//div[@role='listitem' and contains(.,'Course 2')]//span[@data-region='is-favourite' and @aria-hidden='true']" "xpath_element" should not exist
And "//div[@role='listitem' and contains(.,'Course 2')]//span[@data-region='not-favourite' and @aria-hidden='true']" "xpath_element" should exist
And "//div[@role='listitem' and contains(.,'Course 1')]//span[@data-region='is-favourite' and @aria-hidden='true']" "xpath_element" should exist
And "//div[@role='listitem' and contains(.,'Course 3')]//span[@data-region='is-favourite' and @aria-hidden='true']" "xpath_element" should exist
Then "//div[@class='card dashboard-card' and contains(.,'Course 2')]//span[@data-region='is-favourite' and @aria-hidden='false']" "xpath_element" should exist
And "//div[@class='card dashboard-card' and contains(.,'Course 2')]//span[@data-region='is-favourite' and @aria-hidden='true']" "xpath_element" should not exist
And "//div[@class='card dashboard-card' and contains(.,'Course 2')]//span[@data-region='not-favourite' and @aria-hidden='true']" "xpath_element" should exist
And "//div[@class='card dashboard-card' and contains(.,'Course 1')]//span[@data-region='is-favourite' and @aria-hidden='true']" "xpath_element" should exist
And "//div[@class='card dashboard-card' and contains(.,'Course 3')]//span[@data-region='is-favourite' and @aria-hidden='true']" "xpath_element" should exist
And I log out
Scenario: Star a course and switch display to list

View File

@ -0,0 +1,83 @@
@block @block_myoverview @javascript
Feature: The my overview block allows users to hide 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
Background:
Given the following "users" exist:
| username | firstname | lastname | email | idnumber |
| student1 | Student | X | student1@example.com | S1 |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
| Course 2 | C2 | 0 |
| Course 3 | C3 | 0 |
| Course 4 | C4 | 0 |
| Course 5 | C5 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| student1 | C2 | student |
| student1 | C3 | student |
| student1 | C4 | student |
| student1 | C5 | student |
Scenario: Test hide toggle functionality
Given I log in as "student1"
When I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
And I click on "Hide from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
And I reload the page
Then I should not see "Course 2" in the "Course overview" "block"
And I log out
Scenario: Test hide toggle functionality w/ favorites
Given I log in as "student1"
And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
And I click on "Star this course" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
And I click on "Hide from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
When I reload the page
Then I should not see "Course 2" in the "Course overview" "block"
And I click on "All" "button" in the "Course overview" "block"
And I click on "Starred" "link" in the "Course overview" "block"
Then I should not see "Course 2" in the "Course overview" "block"
And I click on "Starred" "button" in the "Course overview" "block"
And I click on "Hidden" "link" in the "Course overview" "block"
Then I should see "Course 2" in the "Course overview" "block"
And I log out
Scenario: Test show toggle functionality
Given I log in as "student1"
And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
And I click on "Hide from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
When I click on "All" "button" in the "Course overview" "block"
And I click on "Hidden" "link" in the "Course overview" "block"
When I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
And I click on "Show this course" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
And I reload the page
And I click on "Hidden" "button" in the "Course overview" "block"
When I click on "All" "link" in the "Course overview" "block"
Then I should see "Course 2" in the "Course overview" "block"
And I log out
Scenario: Test show toggle functionality w/ favorites
Given I log in as "student1"
And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
And I click on "Star this course" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
And I click on "Hide from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
And I click on "All" "button" in the "Course overview" "block"
And I click on "Hidden" "link" in the "Course overview" "block"
And I should see "Course 2" in the "Course overview" "block"
And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
And I click on "Show this course" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
When I reload the page
Then I should not see "Course 2" in the "Course overview" "block"
And I click on "Hidden" "button" in the "Course overview" "block"
And I click on "All" "link" in the "Course overview" "block"
Then I should see "Course 2" in the "Course overview" "block"
And I click on "All" "button" in the "Course overview" "block"
And I click on "Starred" "link" in the "Course overview" "block"
Then I should see "Course 2" in the "Course overview" "block"
And I log out

View File

@ -78,4 +78,23 @@ class block_myoverview_privacy_testcase extends \core_privacy\tests\provider_tes
array('block_myoverview_user_view_preference', 'summary')
);
}
public function test_export_user_preferences_with_hidden_courses() {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$name = "block_myoverview_hidden_course_1";
set_user_preference($name, 1, $user);
provider::export_user_preferences($user->id);
$writer = writer::with_context(\context_system::instance());
$blockpreferences = $writer->get_user_preferences('block_myoverview');
$this->assertEquals(
get_string("privacy:request:preference:set", 'block_myoverview', (object) [
'name' => $name,
'value' => 1,
]),
$blockpreferences->{$name}->description
);
}
}

View File

@ -71,7 +71,8 @@ class course_summary_exporter extends \core\external\exporter {
'courseimage' => $courseimage,
'progress' => $progress,
'hasprogress' => $hasprogress,
'isfavourite' => $this->related['isfavourite']
'isfavourite' => $this->related['isfavourite'],
'hidden' => boolval(get_user_preferences('block_myoverview_hidden_course_' . $this->data->id, 0))
);
}
@ -137,6 +138,9 @@ class course_summary_exporter extends \core\external\exporter {
),
'isfavourite' => array(
'type' => PARAM_BOOL
),
'hidden' => array(
'type' => PARAM_BOOL
)
);
}

View File

@ -3698,6 +3698,8 @@ class core_course_external extends external_api {
break;
case COURSE_FAVOURITES:
break;
case COURSE_TIMELINE_HIDDEN:
break;
default:
throw new invalid_parameter_exception('Invalid classification');
}
@ -3706,7 +3708,17 @@ class core_course_external extends external_api {
$requiredproperties = course_summary_exporter::define_properties();
$fields = join(',', array_keys($requiredproperties));
$courses = course_get_enrolled_courses_for_logged_in_user(0, $offset, $sort, $fields);
$hiddencourses = get_hidden_courses_on_timeline();
$courses = [];
// If the timeline requires the hidden courses then restrict the result to only $hiddencourses else exclude.
if ($classification == COURSE_TIMELINE_HIDDEN) {
$courses = course_get_enrolled_courses_for_logged_in_user(0, $offset, $sort, $fields,
COURSE_DB_QUERY_LIMIT, $hiddencourses);
} else {
$courses = course_get_enrolled_courses_for_logged_in_user(0, $offset, $sort, $fields,
COURSE_DB_QUERY_LIMIT, [], $hiddencourses);
}
$favouritecourseids = [];
$ufservice = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($USER->id));
@ -3725,7 +3737,6 @@ class core_course_external extends external_api {
$favouritecourseids,
$limit
);
} else {
list($filteredcourses, $processedcount) = course_filter_courses_by_timeline_classification(
$courses,

View File

@ -60,6 +60,7 @@ define('COURSE_TIMELINE_PAST', 'past');
define('COURSE_TIMELINE_INPROGRESS', 'inprogress');
define('COURSE_TIMELINE_FUTURE', 'future');
define('COURSE_FAVOURITES', 'favourites');
define('COURSE_TIMELINE_HIDDEN', 'hidden');
define('COURSE_DB_QUERY_LIMIT', 1000);
function make_log_url($module, $url) {
@ -4190,6 +4191,8 @@ function course_classify_courses_for_timeline(array $courses) {
* @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
* @param array $includecourses courses ids to be restricted
* @param array $hiddencourses courses ids to be excluded
* @return Generator
*/
function course_get_enrolled_courses_for_logged_in_user(
@ -4197,14 +4200,16 @@ function course_get_enrolled_courses_for_logged_in_user(
int $offset = 0,
string $sort = null,
string $fields = null,
int $dbquerylimit = COURSE_DB_QUERY_LIMIT
int $dbquerylimit = COURSE_DB_QUERY_LIMIT,
array $includecourses = [],
array $hiddencourses = []
) : Generator {
$haslimit = !empty($limit);
$recordsloaded = 0;
$querylimit = (!$haslimit || $limit > $dbquerylimit) ? $dbquerylimit : $limit;
while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, [], false, $offset)) {
while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, $includecourses, false, $offset, $hiddencourses)) {
yield from $courses;
$recordsloaded += $querylimit;
@ -4242,7 +4247,8 @@ function course_filter_courses_by_timeline_classification(
) : array {
if (!in_array($classification,
[COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, COURSE_TIMELINE_INPROGRESS, COURSE_TIMELINE_FUTURE])) {
[COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, COURSE_TIMELINE_INPROGRESS,
COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_HIDDEN])) {
$message = 'Classification must be one of COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, '
. 'COURSE_TIMELINE_INPROGRESS or COURSE_TIMELINE_FUTURE';
throw new moodle_exception($message);
@ -4254,8 +4260,11 @@ function course_filter_courses_by_timeline_classification(
foreach ($courses as $course) {
$numberofcoursesprocessed++;
$pref = get_user_preferences('block_myoverview_hidden_course_' . $course->id, 0);
if ($classification == COURSE_TIMELINE_ALL || $classification == course_classify_for_timeline($course)) {
// Added as of MDL-63457 toggle viewability for each user.
if (($classification == COURSE_TIMELINE_HIDDEN && $pref) ||
(($classification == COURSE_TIMELINE_ALL || $classification == course_classify_for_timeline($course)) && !$pref)) {
$filteredcourses[] = $course;
$filtermatches++;
}
@ -4494,3 +4503,29 @@ function can_download_from_backup_filearea($filearea, \context $context, stdClas
}
return $candownload;
}
/**
* Get a list of hidden courses
*
* @param int|object|null $user User override to get the filter from. Defaults to current user
* @return array $ids List of hidden courses
* @throws coding_exception
*/
function get_hidden_courses_on_timeline($user = null) {
global $USER;
if (empty($user)) {
$user = $USER->id;
}
$preferences = get_user_preferences(null, null, $user);
$ids = [];
foreach ($preferences as $key => $value) {
if (preg_match('/block_myoverview_hidden_course_(\d)+/', $key)) {
$id = preg_split('/block_myoverview_hidden_course_/', $key);
$ids[] = $id[1];
}
}
return $ids;
}

View File

@ -4718,6 +4718,218 @@ class core_course_courselib_testcase extends advanced_testcase {
$this->assertEquals($expectedprocessedcount, $processedcount);
}
/**
* Test cases for the course_filter_courses_by_timeline_classification w/ hidden courses tests.
*/
public function get_course_filter_courses_by_timeline_classification_hidden_courses_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,
'hiddencourse' => ''
],
// COURSE_TIMELINE_FUTURE.
'future not limit no offset' => [
'coursedata' => $coursedata,
'classification' => COURSE_TIMELINE_FUTURE,
'limit' => 0,
'offset' => 0,
'expectedcourses' => ['afuture', 'cfuture', 'dfuture', 'efuture'],
'expectedprocessedcount' => 15,
'hiddencourse' => 'bfuture'
],
'future no offset' => [
'coursedata' => $coursedata,
'classification' => COURSE_TIMELINE_FUTURE,
'limit' => 2,
'offset' => 0,
'expectedcourses' => ['afuture', 'cfuture'],
'expectedprocessedcount' => 7,
'hiddencourse' => 'bfuture'
],
'future offset' => [
'coursedata' => $coursedata,
'classification' => COURSE_TIMELINE_FUTURE,
'limit' => 2,
'offset' => 2,
'expectedcourses' => ['bfuture', 'dfuture'],
'expectedprocessedcount' => 8,
'hiddencourse' => 'cfuture'
],
'future exact limit' => [
'coursedata' => $coursedata,
'classification' => COURSE_TIMELINE_FUTURE,
'limit' => 5,
'offset' => 0,
'expectedcourses' => ['afuture', 'cfuture', 'dfuture', 'efuture'],
'expectedprocessedcount' => 15,
'hiddencourse' => 'bfuture'
],
'future limit less results' => [
'coursedata' => $coursedata,
'classification' => COURSE_TIMELINE_FUTURE,
'limit' => 10,
'offset' => 0,
'expectedcourses' => ['afuture', 'cfuture', 'dfuture', 'efuture'],
'expectedprocessedcount' => 15,
'hiddencourse' => 'bfuture'
],
'future limit less results with offset' => [
'coursedata' => $coursedata,
'classification' => COURSE_TIMELINE_FUTURE,
'limit' => 10,
'offset' => 5,
'expectedcourses' => ['cfuture', 'efuture'],
'expectedprocessedcount' => 10,
'hiddencourse' => 'dfuture'
],
];
}
/**
* Test the course_filter_courses_by_timeline_classification function hidden courses.
*
* @dataProvider get_course_filter_courses_by_timeline_classification_hidden_courses_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.
* @param int $hiddencourse The course to hide as part of this process
*/
public function test_course_filter_courses_by_timeline_classification_with_hidden_courses(
$coursedata,
$classification,
$limit,
$offset,
$expectedcourses,
$expectedprocessedcount,
$hiddencourse
) {
$this->resetAfterTest();
$generator = $this->getDataGenerator();
$student = $generator->create_user();
$this->setUser($student);
$courses = array_map(function($coursedata) use ($generator, $hiddencourse) {
$course = $generator->create_course($coursedata);
if ($course->shortname == $hiddencourse) {
set_user_preference('block_myoverview_hidden_course_' . $course->id, true);
}
return $course;
}, $coursedata);
foreach ($courses as $course) {
$generator->enrol_user($student->id, $course->id, '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);
}
/**
* Testing core_course_core_calendar_get_valid_event_timestart_range when the course has no end date.

View File

@ -604,6 +604,60 @@ class core_enrollib_testcase extends advanced_testcase {
$this->assertEquals($course2->id, $courses[$course2->id]->id);
}
/**
* Tests the enrol_get_my_courses function when using the $includehidden parameter, which
* should remove any courses hidden from the user's timeline
*
* @throws coding_exception
* @throws dml_exception
*/
public function test_enrol_get_my_courses_include_hidden() {
global $DB, $CFG;
$this->resetAfterTest(true);
// Create test user and 4 courses, two of which have guest access enabled.
$user = $this->getDataGenerator()->create_user();
$course1 = $this->getDataGenerator()->create_course(
(object)array('shortname' => 'X',
'enrol_guest_status_0' => ENROL_INSTANCE_DISABLED,
'enrol_guest_password_0' => ''));
$course2 = $this->getDataGenerator()->create_course(
(object)array('shortname' => 'Z',
'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
'enrol_guest_password_0' => ''));
$course3 = $this->getDataGenerator()->create_course(
(object)array('shortname' => 'Y',
'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
'enrol_guest_password_0' => 'frog'));
$course4 = $this->getDataGenerator()->create_course(
(object)array('shortname' => 'W',
'enrol_guest_status_0' => ENROL_INSTANCE_DISABLED,
'enrol_guest_password_0' => ''));
// User is enrolled in first course.
$this->getDataGenerator()->enrol_user($user->id, $course1->id);
$this->getDataGenerator()->enrol_user($user->id, $course2->id);
$this->getDataGenerator()->enrol_user($user->id, $course3->id);
$this->getDataGenerator()->enrol_user($user->id, $course4->id);
// Check enrol_get_my_courses basic use (without include hidden provided).
$this->setUser($user);
$courses = enrol_get_my_courses();
$this->assertEquals([$course4->id, $course3->id, $course2->id, $course1->id], array_keys($courses));
// Hide a course.
set_user_preference('block_myoverview_hidden_course_' . $course3->id, true);
// Hidden course shouldn't be returned.
$courses = enrol_get_my_courses(null, null, 0, [], false, 0, [$course3->id]);
$this->assertEquals([$course4->id, $course2->id, $course1->id], array_keys($courses));
// Offset should take into account hidden course.
$courses = enrol_get_my_courses(null, null, 0, [], false, 2, [$course3->id]);
$this->assertEquals([$course1->id], array_keys($courses));
}
/**
* Tests the enrol_get_my_courses function when using the $allaccessible parameter, which
* includes a wider range of courses (enrolled courses + other accessible ones).

View File

@ -1 +1 @@
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}});
define(["jquery","core/templates","core/notification","core/paged_content","core/paged_content_events","core/pubsub"],function(a,b,c,d,e,f){var g={PAGED_CONTENT:"core/paged_content"},h={ITEMS_PER_PAGE_SINGLE:25,ITEMS_PER_PAGE_ARRAY:[25,50,100,0],MAX_PAGES:3},i=function(){return{pagingbar:!1,pagingdropdown:!1,skipjs:!0,ignorecontrolwhileloading:!0,controlplacementbottom:!1}},j=function(){return{showitemsperpageselector:!1,itemsperpage:35,previous:!0,next:!0,activepagenumber:1,hidecontrolonsinglepage:!0,pages:[]}},k=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},l=function(b,c){null===c&&(c=h.ITEMS_PER_PAGE_SINGLE),a.isArray(c)&&(c=c[0]);var d=j();d.itemsperpage=c;for(var e=k(b,c),f=1;f<=e;f++){var g={number:f,page:""+f};1===f&&(g.active=!0),d.pages.push(g)}return d},m=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},n=function(b){null===b&&(b=h.ITEMS_PER_PAGE_ARRAY);var c=j();return c.itemsperpage=m(b),c.showitemsperpageselector=a.isArray(b),c},o=function(a,b){return a?l(a,b):n(b)},p=function(b,c){if(null===b&&(b=h.ITEMS_PER_PAGE_SINGLE),a.isArray(b))return{options:b};var d={options:[]},e=0,f=0,g=h.MAX_PAGES;c.hasOwnProperty("maxPages")&&(g=c.maxPages);for(var i=1;i<=g;i++){var j=0;i<=2?(j=b,f=b):(f=2*f,j=f),e+=j;var k={itemcount:j,content:e};1===i&&(k.active=!0),d.options.push(k)}return d},q=function(a,b,c){var d=i();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=p(b,c):d.pagingbar=o(a,b),d},r=function(a,b){return t(null,null,a,b)},s=function(a,b,c){return t(null,a,b,c)},t=function(e,f,h,i){i=i||{};var j=a.Deferred(),k=q(e,f,i);return b.render(g.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()},u=function(a,b,c,d){"undefined"==typeof d&&(d={});var e=a.length;return t(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)},v=function(a,b){f.publish(a+e.ALL_ITEMS_LOADED,b)};return{create:r,createWithLimit:s,createWithTotalAndLimit:t,createFromStaticList:u,createFromAjax:t,resetLastPageNumber:v}});

View File

@ -1 +1 @@
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}});
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(),p=!0;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 q=l(n,{allItemsLoaded:function(a){d.publish(h+e.ALL_ITEMS_LOADED,a)}}),r=q.map(function(a,c){return k(b,a,n[c].pageNumber)});a.when.apply(a,r).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 s=j(b);o.then(function(a){var c=m.concat(a);b.find(f.PAGE_REGION).addClass("hidden"),c.forEach(function(a){p&&a.removeClass("hidden")})}).then(function(){d.publish(h+e.PAGES_SHOWN,g)}).fail(c.exception).always(function(){s.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}});

View File

@ -25,13 +25,17 @@ define(
'jquery',
'core/templates',
'core/notification',
'core/paged_content'
'core/paged_content',
'core/paged_content_events',
'core/pubsub'
],
function(
$,
Templates,
Notification,
PagedContent
PagedContent,
PagedContentEvents,
PubSub
) {
var TEMPLATES = {
PAGED_CONTENT: 'core/paged_content'
@ -479,12 +483,24 @@ function(
}, config);
};
/**
* Reset the last page number for the generated paged-content
* This is used when we need a way to update the last page number outside of the getters callback
*
* @param {String} id ID of the paged content container
* @param {Int} lastPageNumber The last page number
*/
var resetLastPageNumber = function(id, lastPageNumber) {
PubSub.publish(id + PagedContentEvents.ALL_ITEMS_LOADED, lastPageNumber);
};
return {
create: create,
createWithLimit: createWithLimit,
createWithTotalAndLimit: createWithTotalAndLimit,
createFromStaticList: createFromStaticList,
// Backwards compatibility just in case anyone was using this.
createFromAjax: createWithTotalAndLimit
createFromAjax: createWithTotalAndLimit,
resetLastPageNumber: resetLastPageNumber
};
});

View File

@ -185,7 +185,7 @@ define(
var existingPages = [];
var newPageData = [];
var newPagesPromise = $.Deferred();
var shownewpage = true;
// Check which of the pages being requests have previously been rendered
// so that we only ask for new pages to be rendered by the callback.
pagesData.forEach(function(pageData) {
@ -239,9 +239,11 @@ define(
var pagesToShow = existingPages.concat(newPages);
// Hide all existing pages.
root.find(SELECTORS.PAGE_REGION).addClass('hidden');
// Show each of the pages that were requested.
// Show each of the pages that were requested.;
pagesToShow.forEach(function(page) {
page.removeClass('hidden');
if (shownewpage) {
page.removeClass('hidden');
}
});
return;

View File

@ -560,9 +560,11 @@ function enrol_add_course_navigation(navigation_node $coursenode, $course) {
* @param array $courseids the list of course ids to filter by
* @param bool $allaccessible Include courses user is not enrolled in, but can access
* @param int $offset Offset the result set by this number
* @param array $excludecourses IDs of hidden courses to exclude from search
* @return array
*/
function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $courseids = [], $allaccessible = false, $offset = 0) {
function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $courseids = [], $allaccessible = false,
$offset = 0, $excludecourses = []) {
global $DB, $USER, $CFG;
if ($sort === null) {
@ -654,6 +656,12 @@ function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $coursei
$params = array_merge($params, $courseidsparams);
}
if (!empty($excludecourses)) {
list($courseidssql, $courseidsparams) = $DB->get_in_or_equal($excludecourses, SQL_PARAMS_NAMED, 'param', false);
$wheres = sprintf("%s AND c.id %s", $wheres, $courseidssql);
$params = array_merge($params, $courseidsparams);
}
$courseidsql = "";
// Logged-in, non-guest users get their enrolled courses.
if (!isguestuser() && isloggedin()) {

View File

@ -21,7 +21,8 @@
Example context (json):
{
"isfavourite": true
"isfavourite": true,
"hidden": true
}
}}
<div class="ml-auto dropdown">
@ -46,6 +47,16 @@
{{#str}} removefromfavourites, block_myoverview {{/str}}
</a>
</li>
<li class="{{^hidden}}hidden{{/hidden}}" data-action="hide-course" data-course-id="{{id}}">
<a class="dropdown-item p-a-1" href="#">
{{#str}} aria:hidecourse, block_myoverview, {{fullname}} {{/str}}
</a>
</li>
<li class="{{#hidden}}hidden{{/hidden}}" data-action="show-course" data-course-id="{{id}}">
<a class="dropdown-item p-a-1" href="#">
{{#str}} aria:showcourse, block_myoverview, {{fullname}} {{/str}}
</a>
</li>
</ul>
</div>
</div>