MDL-73547 core_courseformat: add collapsed section update actions

Now content and course index sections have special mutations to store
the collapsed preferences. This way the backend implementation is
independent of the frontend one and can use caches or other kind of
optimizations of necessary.
This commit is contained in:
Ferran Recio 2022-02-21 11:28:17 +01:00
parent c64bef51a9
commit 3651b85fbe
13 changed files with 161 additions and 77 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -156,11 +156,9 @@ export default class Component extends BaseComponent {
// Update the state.
const sectionId = section.getAttribute('data-id');
this.reactive.dispatch(
'sectionPreferences',
'sectionContentCollapsed',
[sectionId],
{
contentcollapsed: !isCollapsed,
},
!isCollapsed
);
}
}
@ -182,11 +180,9 @@ export default class Component extends BaseComponent {
const course = this.reactive.get('course');
this.reactive.dispatch(
'sectionPreferences',
'sectionContentCollapsed',
course.sectionlist ?? [],
{
contentcollapsed: !isAllCollapsed,
}
!isAllCollapsed
);
}

View File

@ -286,14 +286,42 @@ export default class {
stateManager.setReadOnly(true);
}
/*
* Get updated user preferences and state data related to some section ids.
/**
* Update the course index collapsed attribute of some sections.
*
* @param {StateManager} stateManager the current state
* @param {array} sectionIds the list of section ids to update
* @param {Object} preferences the new preferences values
* @param {StateManager} stateManager the current state manager
* @param {array} sectionIds the affected section ids
* @param {boolean} collapsed the new collapsed value
*/
async sectionPreferences(stateManager, sectionIds, preferences) {
async sectionIndexCollapsed(stateManager, sectionIds, collapsed) {
const collapsedIds = this._updateStateSectionPreference(stateManager, 'indexcollapsed', sectionIds, collapsed);
const course = stateManager.get('course');
await this._callEditWebservice('section_index_collapsed', course.id, collapsedIds);
}
/**
* Update the course content collapsed attribute of some sections.
*
* @param {StateManager} stateManager the current state manager
* @param {array} sectionIds the affected section ids
* @param {boolean} collapsed the new collapsed value
*/
async sectionContentCollapsed(stateManager, sectionIds, collapsed) {
const collapsedIds = this._updateStateSectionPreference(stateManager, 'contentcollapsed', sectionIds, collapsed);
const course = stateManager.get('course');
await this._callEditWebservice('section_content_collapsed', course.id, collapsedIds);
}
/**
* Private batch update for a section preference attribute.
*
* @param {StateManager} stateManager the current state manager
* @param {string} preferenceName the preference name
* @param {array} sectionIds the affected section ids
* @param {boolean} preferenceValue the new preferenceValue value
* @return {array} the list of all sections with that preference set to true
*/
_updateStateSectionPreference(stateManager, preferenceName, sectionIds, preferenceValue) {
stateManager.setReadOnly(false);
const affectedSections = new Set();
// Check if we need to update preferences.
@ -302,41 +330,25 @@ export default class {
if (section === undefined) {
return;
}
let newValue = preferences.contentcollapsed ?? section.contentcollapsed;
if (section.contentcollapsed != newValue) {
section.contentcollapsed = newValue;
affectedSections.add(section.id);
}
newValue = preferences.indexcollapsed ?? section.indexcollapsed;
if (section.indexcollapsed != newValue) {
section.indexcollapsed = newValue;
const newValue = preferenceValue ?? section[preferenceName];
if (section[preferenceName] != newValue) {
section[preferenceName] = newValue;
affectedSections.add(section.id);
}
});
stateManager.setReadOnly(true);
if (affectedSections.size > 0) {
// Build the preference structures.
const course = stateManager.get('course');
const state = stateManager.state;
const prefKey = `coursesectionspreferences_${course.id}`;
const preferences = {
contentcollapsed: [],
indexcollapsed: [],
};
state.section.forEach(section => {
if (section.contentcollapsed) {
preferences.contentcollapsed.push(section.id);
}
if (section.indexcollapsed) {
preferences.indexcollapsed.push(section.id);
}
});
const jsonString = JSON.stringify(preferences);
M.util.set_user_preference(prefKey, jsonString);
// Inform the backend of the change.
await this._callEditWebservice('topic_preferences_updated', course.id, [...affectedSections]);
if (affectedSections.size == 0) {
return [];
}
// Get all collapsed section ids.
const collapsedSectionIds = [];
const state = stateManager.state;
state.section.forEach(section => {
if (section[preferenceName]) {
collapsedSectionIds.push(section.id);
}
});
return collapsedSectionIds;
}
/**

View File

@ -135,11 +135,9 @@ export default class Component extends BaseComponent {
// Update the state.
const sectionId = section.getAttribute('data-id');
this.reactive.dispatch(
'sectionPreferences',
'sectionIndexCollapsed',
[sectionId],
{
indexcollapsed: !isCollapsed,
},
!isCollapsed
);
}
}

View File

@ -550,6 +550,8 @@ abstract class base {
/**
* Return the format section preferences.
*
* @return array of preferences indexed by sectionid
*/
public function get_sections_preferences(): array {
global $USER;
@ -564,17 +566,7 @@ abstract class base {
return $coursesections;
}
// Calculate collapsed preferences.
try {
$sectionpreferences = (array) json_decode(
get_user_preferences('coursesectionspreferences_' . $course->id, null, $USER->id)
);
if (empty($sectionpreferences)) {
$sectionpreferences = [];
}
} catch (\Throwable $e) {
$sectionpreferences = [];
}
$sectionpreferences = $this->get_sections_preferences_by_preference();
foreach ($sectionpreferences as $preference => $sectionids) {
if (!empty($sectionids) && is_array($sectionids)) {
@ -588,10 +580,48 @@ abstract class base {
}
$coursesectionscache->set($course->id, $result);
return $result;
}
/**
* Return the format section preferences.
*
* @return array of preferences indexed by preference name
*/
public function get_sections_preferences_by_preference(): array {
global $USER;
$course = $this->get_course();
try {
$sectionpreferences = (array) json_decode(
get_user_preferences('coursesectionspreferences_' . $course->id, null, $USER->id)
);
if (empty($sectionpreferences)) {
$sectionpreferences = [];
}
} catch (\Throwable $e) {
$sectionpreferences = [];
}
return $sectionpreferences;
}
/**
* Return the format section preferences.
*
* @param string $preferencename preference name
* @param int[] $sectionids affected section ids
*
*/
public function set_sections_preference(string $preferencename, array $sectionids) {
global $USER;
$course = $this->get_course();
$sectionpreferences = $this->get_sections_preferences_by_preference();
$sectionpreferences[$preferencename] = $sectionids;
set_user_preference('coursesectionspreferences_' . $course->id, json_encode($sectionpreferences), $USER->id);
// Invalidate section preferences cache.
$coursesectionscache = cache::make('core', 'coursesectionspreferences');
$coursesectionscache->delete($course->id);
}
/**
* Returns the information about the ajax support in the given source format
*

View File

@ -289,11 +289,7 @@ class stateactions {
}
/**
* Some of the topic preferences has been updated.
*
* Section preferences can be handled by many user actions and even some of them can affect
* only the frontend part. Format plugins can override this method to add extra logic to the
* section preferences.
* Update the course content section collapsed value.
*
* @param stateupdates $updates the affected course elements track
* @param stdClass $course the course object
@ -301,15 +297,41 @@ class stateactions {
* @param int $targetsectionid not used
* @param int $targetcmid not used
*/
public function topic_preferences_updated(
public function section_content_collapsed(
stateupdates $updates,
stdClass $course,
array $ids = [],
?int $targetsectionid = null,
?int $targetcmid = null
): void {
// Format plugins may override this method to provide extra functionalities to
// section preferences.
if (!empty($ids)) {
$this->validate_sections($course, $ids, __FUNCTION__);
}
$format = course_get_format($course->id);
$format->set_sections_preference('contentcollapsed', $ids);
}
/**
* Update the course index section collapsed value.
*
* @param stateupdates $updates the affected course elements track
* @param stdClass $course the course object
* @param int[] $ids the collapsed section ids
* @param int $targetsectionid not used
* @param int $targetcmid not used
*/
public function section_index_collapsed(
stateupdates $updates,
stdClass $course,
array $ids = [],
?int $targetsectionid = null,
?int $targetcmid = null
): void {
if (!empty($ids)) {
$this->validate_sections($course, $ids, __FUNCTION__);
}
$format = course_get_format($course->id);
$format->set_sections_preference('indexcollapsed', $ids);
}
/**

View File

@ -261,9 +261,7 @@ class base_test extends advanced_testcase {
* @covers ::get_sections_preferences
*/
public function test_get_sections_preferences() {
$this->resetAfterTest();
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$user = $generator->create_and_enrol($course, 'student');
@ -289,7 +287,36 @@ class base_test extends advanced_testcase {
(object)['pref1' => true],
$preferences[2]
);
}
/**
* Test for the default delete format data behaviour.
*
* @covers ::set_sections_preference
*/
public function test_set_sections_preference() {
$this->resetAfterTest();
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$user = $generator->create_and_enrol($course, 'student');
$format = course_get_format($course);
$this->setUser($user);
// Load data from user 1.
$format->set_sections_preference('pref1', [1, 2]);
$format->set_sections_preference('pref2', [1]);
$format->set_sections_preference('pref3', []);
$preferences = $format->get_sections_preferences();
$this->assertEquals(
(object)['pref1' => true, 'pref2' => true],
$preferences[1]
);
$this->assertEquals(
(object)['pref1' => true],
$preferences[2]
);
}
/**

View File

@ -66,7 +66,6 @@ class core_course_renderer extends plugin_renderer_base {
public function __construct(moodle_page $page, $target) {
$this->strings = new stdClass;
$courseid = $page->course->id;
user_preference_allow_ajax_update('coursesectionspreferences_' . $courseid, PARAM_RAW);
parent::__construct($page, $target);
}