mirror of
https://github.com/moodle/moodle.git
synced 2025-04-16 05:54:19 +02:00
Merge branch 'MDL-73547-master-v03' of https://github.com/ferranrecio/moodle
This commit is contained in:
commit
faa6c74404
2
course/format/amd/build/courseeditor.min.js
vendored
2
course/format/amd/build/courseeditor.min.js
vendored
@ -5,6 +5,6 @@ define("core_courseformat/courseeditor",["exports","core_courseformat/local/cour
|
||||
* @module core_courseformat/courseeditor
|
||||
* @copyright 2021 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setViewFormat=_exports.getCurrentCourseEditor=_exports.getCourseEditor=void 0,_mutations=_interopRequireDefault(_mutations),_courseeditor=_interopRequireDefault(_courseeditor),_events=_interopRequireDefault(_events);const courseEditorMap=new Map;function dispatchStateChangedEvent(detail,target){void 0===target&&(target=document),target.dispatchEvent(new CustomEvent(_events.default.stateChanged,{bubbles:!0,detail:detail}))}_exports.setViewFormat=(courseId,setup)=>{getCourseEditor(courseId).setViewFormat(setup)};const getCourseEditor=courseId=>(courseId=parseInt(courseId),courseEditorMap.has(courseId)||(courseEditorMap.set(courseId,new _courseeditor.default({name:"CourseEditor".concat(courseId),eventName:_events.default.stateChanged,eventDispatch:dispatchStateChangedEvent,mutations:new _mutations.default})),courseEditorMap.get(courseId).loadCourse(courseId)),courseEditorMap.get(courseId));_exports.getCourseEditor=getCourseEditor;_exports.getCurrentCourseEditor=()=>getCourseEditor(M.cfg.courseId)}));
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setViewFormat=_exports.getCurrentCourseEditor=_exports.getCourseEditor=void 0,_mutations=_interopRequireDefault(_mutations),_courseeditor=_interopRequireDefault(_courseeditor),_events=_interopRequireDefault(_events);const courseEditorMap=new Map,courseStateKeyMap=new Map;function dispatchStateChangedEvent(detail,target){void 0===target&&(target=document),target.dispatchEvent(new CustomEvent(_events.default.stateChanged,{bubbles:!0,detail:detail}))}_exports.setViewFormat=(courseId,setup)=>{courseId=parseInt(courseId),setup.editing||courseStateKeyMap.set(courseId,setup.statekey);getCourseEditor(courseId).setViewFormat(setup)};const getCourseEditor=courseId=>(courseId=parseInt(courseId),courseEditorMap.has(courseId)||(courseEditorMap.set(courseId,new _courseeditor.default({name:"CourseEditor".concat(courseId),eventName:_events.default.stateChanged,eventDispatch:dispatchStateChangedEvent,mutations:new _mutations.default})),courseEditorMap.get(courseId).loadCourse(courseId,courseStateKeyMap.get(courseId))),courseEditorMap.get(courseId));_exports.getCourseEditor=getCourseEditor;_exports.getCurrentCourseEditor=()=>getCourseEditor(M.cfg.courseId)}));
|
||||
|
||||
//# sourceMappingURL=courseeditor.min.js.map
|
File diff suppressed because one or more lines are too long
2
course/format/amd/build/local/content.min.js
vendored
2
course/format/amd/build/local/content.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -9,6 +9,6 @@ define("core_courseformat/local/courseeditor/courseeditor",["exports","core/reac
|
||||
* @class core_courseformat/local/courseeditor/courseeditor
|
||||
* @copyright 2021 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_notification=_interopRequireDefault(_notification),_exporter=_interopRequireDefault(_exporter),_log=_interopRequireDefault(_log),_ajax=_interopRequireDefault(_ajax),Storage=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Storage);class _default extends _reactive.Reactive{constructor(){super(...arguments),_defineProperty(this,"stateKey",1),_defineProperty(this,"sectionReturn",0)}async loadCourse(courseId){if(this.courseId)throw new Error("Cannot load ".concat(courseId,", course already loaded with id ").concat(this.courseId));let stateData;this._editing=!1,this._supportscomponents=!1,this.courseId=courseId;try{stateData=await this.getServerCourseState()}catch(error){return _log.default.error("EXCEPTION RAISED WHILE INIT COURSE EDITOR"),void _log.default.error(error)}if(this.setInitialState(stateData),this.isEditing)this.stateKey=null;else{const newState=JSON.stringify(stateData);Storage.get("course/".concat(courseId,"/staticState"))!==newState&&(Storage.set("course/".concat(courseId,"/staticState"),newState),Storage.set("course/".concat(courseId,"/stateKey"),Date.now())),this.stateKey=Storage.get("course/".concat(courseId,"/stateKey"))}}setViewFormat(setup){var _setup$editing,_setup$supportscompon;this._editing=null!==(_setup$editing=setup.editing)&&void 0!==_setup$editing&&_setup$editing,this._supportscomponents=null!==(_setup$supportscompon=setup.supportscomponents)&&void 0!==_setup$supportscompon&&_setup$supportscompon}async getServerCourseState(){const courseState=await _ajax.default.call([{methodname:"core_courseformat_get_state",args:{courseid:this.courseId}}])[0];return{course:{},section:[],cm:[],...JSON.parse(courseState)}}get isEditing(){var _this$_editing;return null!==(_this$_editing=this._editing)&&void 0!==_this$_editing&&_this$_editing}getExporter(){return new _exporter.default(this)}get supportComponents(){var _this$_supportscompon;return null!==(_this$_supportscompon=this._supportscomponents)&&void 0!==_this$_supportscompon&&_this$_supportscompon}getStorageValue(key){if(this.isEditing||!this.stateKey)return!1;const dataJson=Storage.get("course/".concat(this.courseId,"/").concat(key));if(!dataJson)return!1;try{const data=JSON.parse(dataJson);return(null==data?void 0:data.stateKey)===this.stateKey&&data.value}catch(error){return!1}}setStorageValue(key,value){if(this.isEditing)return!1;const data={stateKey:this.stateKey,value:value};return Storage.set("course/".concat(this.courseId,"/").concat(key),JSON.stringify(data))}async dispatch(){try{await super.dispatch(...arguments)}catch(error){_notification.default.exception(error),super.dispatch("unlockAll")}}}return _exports.default=_default,_exports.default}));
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_notification=_interopRequireDefault(_notification),_exporter=_interopRequireDefault(_exporter),_log=_interopRequireDefault(_log),_ajax=_interopRequireDefault(_ajax),Storage=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Storage);class _default extends _reactive.Reactive{constructor(){super(...arguments),_defineProperty(this,"stateKey",1),_defineProperty(this,"sectionReturn",0)}async loadCourse(courseId,serverStateKey){if(this.courseId)throw new Error("Cannot load ".concat(courseId,", course already loaded with id ").concat(this.courseId));let stateData;serverStateKey||(serverStateKey="invalidStateKey_".concat(Date.now())),this._editing=!1,this._supportscomponents=!1,this.courseId=courseId;const storeStateKey=Storage.get("course/".concat(courseId,"/stateKey"));try{this.isEditing||serverStateKey!=storeStateKey||(stateData=JSON.parse(Storage.get("course/".concat(courseId,"/staticState")))),stateData||(stateData=await this.getServerCourseState())}catch(error){return _log.default.error("EXCEPTION RAISED WHILE INIT COURSE EDITOR"),void _log.default.error(error)}if(this.setInitialState(stateData),this.isEditing)this.stateKey=null;else{const newState=JSON.stringify(stateData);var _stateData$course$sta,_stateData,_stateData$course;if(Storage.get("course/".concat(courseId,"/staticState"))!==newState||storeStateKey!==serverStateKey)Storage.set("course/".concat(courseId,"/staticState"),newState),Storage.set("course/".concat(courseId,"/stateKey"),null!==(_stateData$course$sta=null===(_stateData=stateData)||void 0===_stateData||null===(_stateData$course=_stateData.course)||void 0===_stateData$course?void 0:_stateData$course.statekey)&&void 0!==_stateData$course$sta?_stateData$course$sta:serverStateKey);this.stateKey=Storage.get("course/".concat(courseId,"/stateKey"))}}setViewFormat(setup){var _setup$editing,_setup$supportscompon;this._editing=null!==(_setup$editing=setup.editing)&&void 0!==_setup$editing&&_setup$editing,this._supportscomponents=null!==(_setup$supportscompon=setup.supportscomponents)&&void 0!==_setup$supportscompon&&_setup$supportscompon}async getServerCourseState(){const courseState=await _ajax.default.call([{methodname:"core_courseformat_get_state",args:{courseid:this.courseId}}])[0];return{course:{},section:[],cm:[],...JSON.parse(courseState)}}get isEditing(){var _this$_editing;return null!==(_this$_editing=this._editing)&&void 0!==_this$_editing&&_this$_editing}getExporter(){return new _exporter.default(this)}get supportComponents(){var _this$_supportscompon;return null!==(_this$_supportscompon=this._supportscomponents)&&void 0!==_this$_supportscompon&&_this$_supportscompon}getStorageValue(key){if(this.isEditing||!this.stateKey)return!1;const dataJson=Storage.get("course/".concat(this.courseId,"/").concat(key));if(!dataJson)return!1;try{const data=JSON.parse(dataJson);return(null==data?void 0:data.stateKey)===this.stateKey&&data.value}catch(error){return!1}}setStorageValue(key,value){if(this.isEditing)return!1;const data={stateKey:this.stateKey,value:value};return Storage.set("course/".concat(this.courseId,"/").concat(key),JSON.stringify(data))}async dispatch(){try{await super.dispatch(...arguments)}catch(error){_notification.default.exception(error),super.dispatch("unlockAll")}}}return _exports.default=_default,_exports.default}));
|
||||
|
||||
//# sourceMappingURL=courseeditor.min.js.map
|
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
@ -28,6 +28,9 @@ import events from 'core_course/events';
|
||||
// A map with all the course editor instances.
|
||||
const courseEditorMap = new Map();
|
||||
|
||||
// Map with all the state keys the backend send us to know if the frontend cache is valid or not.
|
||||
const courseStateKeyMap = new Map();
|
||||
|
||||
/**
|
||||
* Trigger a state changed event.
|
||||
*
|
||||
@ -51,12 +54,22 @@ function dispatchStateChangedEvent(detail, target) {
|
||||
/**
|
||||
* Setup the current view settings
|
||||
*
|
||||
* The backend cache state revision is a combination of the course->cacherev, the
|
||||
* user course preferences and completion state. The backend updates that number
|
||||
* everytime some change in the course affects the user course state.
|
||||
*
|
||||
* @param {number} courseId the course id
|
||||
* @param {setup} setup format, page and course settings
|
||||
* @param {boolean} setup.editing if the page is in edit mode
|
||||
* @param {boolean} setup.supportscomponents if the format supports components for content
|
||||
* @param {boolean} setup.statekey the backend cached state revision
|
||||
*/
|
||||
export const setViewFormat = (courseId, setup) => {
|
||||
courseId = parseInt(courseId);
|
||||
// Caches are ignored in edit mode.
|
||||
if (!setup.editing) {
|
||||
courseStateKeyMap.set(courseId, setup.statekey);
|
||||
}
|
||||
const editor = getCourseEditor(courseId);
|
||||
editor.setViewFormat(setup);
|
||||
};
|
||||
@ -82,7 +95,7 @@ export const getCourseEditor = (courseId) => {
|
||||
mutations: new DefaultMutations(),
|
||||
})
|
||||
);
|
||||
courseEditorMap.get(courseId).loadCourse(courseId);
|
||||
courseEditorMap.get(courseId).loadCourse(courseId, courseStateKeyMap.get(courseId));
|
||||
}
|
||||
return courseEditorMap.get(courseId);
|
||||
};
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -59,14 +59,26 @@ export default class extends Reactive {
|
||||
*
|
||||
* The course can only be loaded once per instance. Otherwise an error is thrown.
|
||||
*
|
||||
* The backend can inform the module of the current state key. This key changes every time some
|
||||
* update in the course affect the current user state. Some examples are:
|
||||
* - The course content has been edited
|
||||
* - The user marks some activity as completed
|
||||
* - The user collapses or uncollapses a section (it is stored as a user preference)
|
||||
*
|
||||
* @param {number} courseId course id
|
||||
* @param {string} serverStateKey the current backend course cache reference
|
||||
*/
|
||||
async loadCourse(courseId) {
|
||||
async loadCourse(courseId, serverStateKey) {
|
||||
|
||||
if (this.courseId) {
|
||||
throw new Error(`Cannot load ${courseId}, course already loaded with id ${this.courseId}`);
|
||||
}
|
||||
|
||||
if (!serverStateKey) {
|
||||
// The server state key is not provided, we use a invalid statekey to force reloading.
|
||||
serverStateKey = `invalidStateKey_${Date.now()}`;
|
||||
}
|
||||
|
||||
// Default view format setup.
|
||||
this._editing = false;
|
||||
this._supportscomponents = false;
|
||||
@ -75,8 +87,16 @@ export default class extends Reactive {
|
||||
|
||||
let stateData;
|
||||
|
||||
const storeStateKey = Storage.get(`course/${courseId}/stateKey`);
|
||||
try {
|
||||
stateData = await this.getServerCourseState();
|
||||
// Check if the backend state key is the same we have in our session storage.
|
||||
if (!this.isEditing && serverStateKey == storeStateKey) {
|
||||
stateData = JSON.parse(Storage.get(`course/${courseId}/staticState`));
|
||||
}
|
||||
if (!stateData) {
|
||||
stateData = await this.getServerCourseState();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
log.error("EXCEPTION RAISED WHILE INIT COURSE EDITOR");
|
||||
log.error(error);
|
||||
@ -92,9 +112,9 @@ export default class extends Reactive {
|
||||
// Check if the last state is the same as the cached one.
|
||||
const newState = JSON.stringify(stateData);
|
||||
const previousState = Storage.get(`course/${courseId}/staticState`);
|
||||
if (previousState !== newState) {
|
||||
if (previousState !== newState || storeStateKey !== serverStateKey) {
|
||||
Storage.set(`course/${courseId}/staticState`, newState);
|
||||
Storage.set(`course/${courseId}/stateKey`, Date.now());
|
||||
Storage.set(`course/${courseId}/stateKey`, stateData?.course?.statekey ?? serverStateKey);
|
||||
}
|
||||
this.stateKey = Storage.get(`course/${courseId}/stateKey`);
|
||||
}
|
||||
@ -106,6 +126,7 @@ export default class extends Reactive {
|
||||
* @param {Object} setup format, page and course settings
|
||||
* @param {boolean} setup.editing if the page is in edit mode
|
||||
* @param {boolean} setup.supportscomponents if the format supports components for content
|
||||
* @param {string} setup.cacherev the backend cached state revision
|
||||
*/
|
||||
setViewFormat(setup) {
|
||||
this._editing = setup.editing ?? false;
|
||||
|
@ -286,55 +286,69 @@ 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.
|
||||
let updatePreferences = false;
|
||||
sectionIds.forEach(sectionId => {
|
||||
const section = stateManager.get('section', sectionId);
|
||||
if (section === undefined) {
|
||||
return;
|
||||
}
|
||||
let newValue = preferences.contentcollapsed ?? section.contentcollapsed;
|
||||
if (section.contentcollapsed != newValue) {
|
||||
section.contentcollapsed = newValue;
|
||||
updatePreferences = true;
|
||||
}
|
||||
newValue = preferences.indexcollapsed ?? section.indexcollapsed;
|
||||
if (section.indexcollapsed != newValue) {
|
||||
section.indexcollapsed = newValue;
|
||||
updatePreferences = true;
|
||||
const newValue = preferenceValue ?? section[preferenceName];
|
||||
if (section[preferenceName] != newValue) {
|
||||
section[preferenceName] = newValue;
|
||||
affectedSections.add(section.id);
|
||||
}
|
||||
});
|
||||
stateManager.setReadOnly(true);
|
||||
|
||||
if (updatePreferences) {
|
||||
// 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);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -225,6 +225,48 @@ abstract class base {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the current user course format cache.
|
||||
*
|
||||
* The course format cache resets every time the course cache resets but
|
||||
* also when the user changes their course format preference, complete
|
||||
* an activity...
|
||||
*
|
||||
* @param stdClass $course the course object
|
||||
* @return string the new statekey
|
||||
*/
|
||||
public static function session_cache_reset(stdClass $course): string {
|
||||
$statecache = cache::make('core', 'courseeditorstate');
|
||||
$newkey = $course->cacherev . '_' . time();
|
||||
$statecache->set($course->id, $newkey);
|
||||
return $newkey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current user course format cache key.
|
||||
*
|
||||
* The course format session cache can be used to cache the
|
||||
* user course representation. The statekey will be reset when the
|
||||
* the course state changes. For example when the course is edited,
|
||||
* the user completes an activity or simply some course preference
|
||||
* like collapsing a section happens.
|
||||
*
|
||||
* @param stdClass $course the course object
|
||||
* @return string the current statekey
|
||||
*/
|
||||
public static function session_cache(stdClass $course): string {
|
||||
$statecache = cache::make('core', 'courseeditorstate');
|
||||
$statekey = $statecache->get($course->id);
|
||||
// Validate the statekey code.
|
||||
if (preg_match('/^[0-9]+_[0-9]+$/', $statekey)) {
|
||||
list($cacherev) = explode('_', $statekey);
|
||||
if ($cacherev == $course->cacherev) {
|
||||
return $statekey;
|
||||
}
|
||||
}
|
||||
return self::session_cache_reset($course);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the format name used by this course
|
||||
*
|
||||
@ -508,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;
|
||||
@ -522,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)) {
|
||||
@ -546,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
|
||||
*
|
||||
|
@ -28,6 +28,7 @@ use external_multiple_structure;
|
||||
use moodle_exception;
|
||||
use coding_exception;
|
||||
use context_course;
|
||||
use core_courseformat\base as course_format;
|
||||
|
||||
/**
|
||||
* External secrvie to update the course from the course editor components.
|
||||
@ -135,8 +136,13 @@ class update_course extends external_api {
|
||||
throw new moodle_exception("Invalid course state action $action in ".get_class($actions));
|
||||
}
|
||||
|
||||
$course = $courseformat->get_course();
|
||||
|
||||
// Execute the action.
|
||||
$actions->$action($updates, $courseformat->get_course(), $ids, $targetsectionid, $targetcmid);
|
||||
$actions->$action($updates, $course, $ids, $targetsectionid, $targetcmid);
|
||||
|
||||
// Any state action mark the state cache as dirty.
|
||||
course_format::session_cache_reset($course);
|
||||
|
||||
return json_encode($updates);
|
||||
}
|
||||
|
@ -65,6 +65,7 @@ class course implements renderable {
|
||||
'highlighted' => $format->get_section_highlighted_name(),
|
||||
'maxsections' => $format->get_max_sections(),
|
||||
'baseurl' => $url->out(),
|
||||
'statekey' => course_format::session_cache($course),
|
||||
];
|
||||
|
||||
$sections = $modinfo->get_section_info_all();
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
namespace core_courseformat;
|
||||
|
||||
use core_courseformat\base as course_format;
|
||||
use core_courseformat\stateupdates;
|
||||
use cm_info;
|
||||
use section_info;
|
||||
@ -24,6 +25,7 @@ use course_modinfo;
|
||||
use moodle_exception;
|
||||
use context_module;
|
||||
use context_course;
|
||||
use cache;
|
||||
|
||||
/**
|
||||
* Contains the core course state actions.
|
||||
@ -286,6 +288,52 @@ class stateactions {
|
||||
return $sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the course content 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_content_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('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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the update messages of the updated version of any cm and section related to the cm ids.
|
||||
*
|
||||
|
@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3311,10 +3311,13 @@ function include_course_editor(course_format $format) {
|
||||
return;
|
||||
}
|
||||
|
||||
$statekey = course_format::session_cache($course);
|
||||
|
||||
// Edition mode and some format specs must be passed to the init method.
|
||||
$setup = (object)[
|
||||
'editing' => $format->show_editor(),
|
||||
'supportscomponents' => $format->supports_components(),
|
||||
'statekey' => $statekey,
|
||||
];
|
||||
// All the new editor elements will be loaded after the course is presented and
|
||||
// the initial course state will be generated using core_course_get_state webservice.
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -52,6 +52,7 @@ $string['cachedef_coursecattree'] = 'Course categories tree';
|
||||
$string['cachedef_coursecompletion'] = 'Course completion status';
|
||||
$string['cachedef_coursecontacts'] = 'List of course contacts';
|
||||
$string['cachedef_coursemodinfo'] = 'Accumulated information about modules and sections for each course';
|
||||
$string['cachedef_courseeditorstate'] = 'Session course state cache keys to detect course changes in the frontend';
|
||||
$string['cachedef_course_image'] = 'Course images';
|
||||
$string['cachedef_course_user_dates'] = 'The user dates for courses set to relative dates mode';
|
||||
$string['cachedef_completion'] = 'Activity completion status';
|
||||
|
@ -27,6 +27,7 @@
|
||||
*/
|
||||
|
||||
use core_completion\activity_custom_completion;
|
||||
use core_courseformat\base as course_format;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@ -635,6 +636,12 @@ class completion_info {
|
||||
return;
|
||||
}
|
||||
|
||||
// The activity completion alters the course state cache for this particular user.
|
||||
$course = get_course($cm->course);
|
||||
if ($course) {
|
||||
course_format::session_cache_reset($course);
|
||||
}
|
||||
|
||||
// For auto tracking, if the status is overridden to 'COMPLETION_COMPLETE', then disallow further changes,
|
||||
// unless processing another override.
|
||||
// Basically, we want those activities which have been overridden to COMPLETE to hold state, and those which have been
|
||||
|
@ -211,6 +211,12 @@ $definitions = array(
|
||||
'simplekeys' => true,
|
||||
'ttl' => 3600,
|
||||
),
|
||||
// Course reactive state cache.
|
||||
'courseeditorstate' => [
|
||||
'mode' => cache_store::MODE_SESSION,
|
||||
'simplekeys' => true,
|
||||
'simpledata' => true,
|
||||
],
|
||||
// Used to store data for repositories to avoid repetitive DB queries within one request.
|
||||
'repositories' => array(
|
||||
'mode' => cache_store::MODE_REQUEST,
|
||||
|
@ -29,7 +29,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2022022200.00; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
$version = 2022022200.01; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
$release = '4.0dev+ (Build: 20220222)'; // Human-friendly version name
|
||||
|
Loading…
x
Reference in New Issue
Block a user