Merge branch 'MDL-71795-master' of git://github.com/ferranrecio/moodle

This commit is contained in:
Andrew Nicols 2021-10-04 12:33:52 +08:00 committed by Eloy Lafuente (stronk7)
commit 8b7fb0f7ab
11 changed files with 151 additions and 17 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
define ("core_courseformat/local/courseindex/placeholder",["exports","core/reactive","core/templates","core_courseformat/courseeditor"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;c=function(a){return a&&a.__esModule?a:{default:a}}(c);function e(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){e=function(a){return typeof a}}else{e=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return e(a)}function f(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function g(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var i=a.apply(b,c);function g(a){f(i,d,e,g,h,"next",a)}function h(a){f(i,d,e,g,h,"throw",a)}g(void 0)})}}function h(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function i(a,b){for(var c=0,d;c<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function j(a,b,c){if(b)i(a.prototype,b);if(c)i(a,c);return a}function k(a,b){if("function"!=typeof b&&null!==b){throw new TypeError("Super expression must either be null or a function")}a.prototype=Object.create(b&&b.prototype,{constructor:{value:a,writable:!0,configurable:!0}});if(b)l(a,b)}function l(a,b){l=Object.setPrototypeOf||function(a,b){a.__proto__=b;return a};return l(a,b)}function m(a){return function(){var b=q(a),c;if(p()){var d=q(this).constructor;c=Reflect.construct(b,arguments,d)}else{c=b.apply(this,arguments)}return n(this,c)}}function n(a,b){if(b&&("object"===e(b)||"function"==typeof b)){return b}return o(a)}function o(a){if(void 0===a){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return a}function p(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return!0}catch(a){return!1}}function q(a){q=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return q(a)}var r=function(a){k(b,a);var e=m(b);function b(){h(this,b);return e.apply(this,arguments)}j(b,[{key:"stateReady",value:function(){var a=g(regeneratorRuntime.mark(function a(b){var d,e,f,g,h;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:d=this.reactive.getExporter();e=d.course(b);a.prev=2;a.next=5;return c.default.renderForPromise("core_courseformat/local/courseindex/courseindex",e);case 5:f=a.sent;g=f.html;h=f.js;c.default.replaceNode(this.element,g,h);a.next=14;break;case 11:a.prev=11;a.t0=a["catch"](2);throw a.t0;case 14:case"end":return a.stop();}}},a,this,[[2,11]])}));return function stateReady(){return a.apply(this,arguments)}}()}],[{key:"init",value:function init(a,c){return new b({element:document.getElementById(a),reactive:(0,d.getCurrentCourseEditor)(),selectors:c})}}]);return b}(b.BaseComponent);a.default=r;return a.default}); define ("core_courseformat/local/courseindex/placeholder",["exports","core/reactive","core/templates","core_courseformat/courseeditor","core/pending"],function(a,b,c,d,e){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;c=f(c);e=f(e);function f(a){return a&&a.__esModule?a:{default:a}}function g(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){g=function(a){return typeof a}}else{g=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return g(a)}function h(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function i(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var i=a.apply(b,c);function f(a){h(i,d,e,f,g,"next",a)}function g(a){h(i,d,e,f,g,"throw",a)}f(void 0)})}}function j(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function k(a,b){for(var c=0,d;c<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function l(a,b,c){if(b)k(a.prototype,b);if(c)k(a,c);return a}function m(a,b){if("function"!=typeof b&&null!==b){throw new TypeError("Super expression must either be null or a function")}a.prototype=Object.create(b&&b.prototype,{constructor:{value:a,writable:!0,configurable:!0}});if(b)n(a,b)}function n(a,b){n=Object.setPrototypeOf||function(a,b){a.__proto__=b;return a};return n(a,b)}function o(a){return function(){var b=s(a),c;if(r()){var d=s(this).constructor;c=Reflect.construct(b,arguments,d)}else{c=b.apply(this,arguments)}return p(this,c)}}function p(a,b){if(b&&("object"===g(b)||"function"==typeof b)){return b}return q(a)}function q(a){if(void 0===a){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return a}function r(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return!0}catch(a){return!1}}function s(a){s=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return s(a)}var t=function(a){m(b,a);var f=o(b);function b(){j(this,b);return f.apply(this,arguments)}l(b,[{key:"create",value:function create(){this.pendingContent=new e.default("core_courseformat/placeholder:loadcourseindex")}},{key:"stateReady",value:function(){var a=i(regeneratorRuntime.mark(function a(b){return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:if(this.loadStaticContent()){a.next=3;break}a.next=3;return this.loadTemplateContent(b);case 3:case"end":return a.stop();}}},a,this)}));return function stateReady(){return a.apply(this,arguments)}}()},{key:"loadStaticContent",value:function loadStaticContent(){var a=this.reactive.getStorageValue("courseIndex");if(a.html&&a.js){c.default.replaceNode(this.element,a.html,a.js);this.pendingContent.resolve();return!0}return!1}},{key:"loadTemplateContent",value:function(){var a=i(regeneratorRuntime.mark(function a(b){var d,e,f,g,h;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:d=this.reactive.getExporter();e=d.course(b);a.prev=2;a.next=5;return c.default.renderForPromise("core_courseformat/local/courseindex/courseindex",e);case 5:f=a.sent;g=f.html;h=f.js;c.default.replaceNode(this.element,g,h);this.pendingContent.resolve();this.reactive.setStorageValue("courseIndex",{html:g,js:h});a.next=17;break;case 13:a.prev=13;a.t0=a["catch"](2);this.pendingContent.resolve(a.t0);throw a.t0;case 17:case"end":return a.stop();}}},a,this,[[2,13]])}));return function loadTemplateContent(){return a.apply(this,arguments)}}()}],[{key:"init",value:function init(a,c){return new b({element:document.getElementById(a),reactive:(0,d.getCurrentCourseEditor)(),selectors:c})}}]);return b}(b.BaseComponent);a.default=t;return a.default});
//# sourceMappingURL=placeholder.min.js.map //# sourceMappingURL=placeholder.min.js.map

File diff suppressed because one or more lines are too long

View file

@ -18,6 +18,7 @@ import notification from 'core/notification';
import Exporter from 'core_courseformat/local/courseeditor/exporter'; import Exporter from 'core_courseformat/local/courseeditor/exporter';
import log from 'core/log'; import log from 'core/log';
import ajax from 'core/ajax'; import ajax from 'core/ajax';
import * as Storage from 'core/sessionstorage';
/** /**
* Main course editor module. * Main course editor module.
@ -32,6 +33,19 @@ import ajax from 'core/ajax';
*/ */
export default class extends Reactive { export default class extends Reactive {
/**
* The current state cache key
*
* The state cache is considered dirty if the state changes from the last page or
* if the page has editing mode on.
*
* @attribute stateKey
* @type number|null
* @default 1
* @package
*/
stateKey = 1;
/** /**
* Set up the course editor when the page is ready. * Set up the course editor when the page is ready.
* *
@ -62,6 +76,20 @@ export default class extends Reactive {
} }
this.setInitialState(stateData); this.setInitialState(stateData);
// In editing mode, the session cache is considered dirty always.
if (this.isEditing) {
this.stateKey = null;
} else {
// 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) {
Storage.set(`course/${courseId}/staticState`, newState);
Storage.set(`course/${courseId}/stateKey`, Date.now());
}
this.stateKey = Storage.get(`course/${courseId}/stateKey`);
}
} }
/** /**
@ -128,6 +156,56 @@ export default class extends Reactive {
return this._supportscomponents ?? false; return this._supportscomponents ?? false;
} }
/**
* Get a value from the course editor static storage if any.
*
* The course editor static storage uses the sessionStorage to store values from the
* components. This is used to prevent unnecesary template loadings on every page. However,
* the storage does not work if no sessionStorage can be used (in debug mode for example),
* if the page is in editing mode or if the initial state change from the last page.
*
* @param {string} key the key to get
* @return {boolean|string} the storage value or false if cannot be loaded
*/
getStorageValue(key) {
if (this.isEditing || !this.stateKey) {
return false;
}
const dataJson = Storage.get(`course/${this.courseId}/${key}`);
if (!dataJson) {
return false;
}
// Check the stateKey.
try {
const data = JSON.parse(dataJson);
if (data?.stateKey !== this.stateKey) {
return false;
}
return data.value;
} catch (error) {
return false;
}
}
/**
* Stores a value into the course editor static storage if available
*
* @param {String} key the key to store
* @param {*} value the value to store (must be compatible with JSON,stringify)
* @returns {boolean} true if the value is stored
*/
setStorageValue(key, value) {
// Values cannot be stored on edit mode.
if (this.isEditing) {
return false;
}
const data = {
stateKey: this.stateKey,
value,
};
return Storage.set(`course/${this.courseId}/${key}`, JSON.stringify(data));
}
/** /**
* Dispatch a change in the state. * Dispatch a change in the state.
* *

View file

@ -25,6 +25,7 @@
import {BaseComponent} from 'core/reactive'; import {BaseComponent} from 'core/reactive';
import Templates from 'core/templates'; import Templates from 'core/templates';
import {getCurrentCourseEditor} from 'core_courseformat/courseeditor'; import {getCurrentCourseEditor} from 'core_courseformat/courseeditor';
import Pending from 'core/pending';
export default class Component extends BaseComponent { export default class Component extends BaseComponent {
@ -43,6 +44,14 @@ export default class Component extends BaseComponent {
}); });
} }
/**
* Component creation hook.
*/
create() {
// Add a pending operation waiting for the initial content.
this.pendingContent = new Pending(`core_courseformat/placeholder:loadcourseindex`);
}
/** /**
* Initial state ready method. * Initial state ready method.
* *
@ -51,10 +60,38 @@ export default class Component extends BaseComponent {
* @param {object} state the initial state * @param {object} state the initial state
*/ */
async stateReady(state) { async stateReady(state) {
// Check if we have a static course index already loded from a previous page.
if (!this.loadStaticContent()) {
await this.loadTemplateContent(state);
}
}
/**
* Load the course index from the session storage if any.
*
* @return {boolean} true if the static version is loaded form the session
*/
loadStaticContent() {
// Load the previous static course index from the session cache.
const index = this.reactive.getStorageValue(`courseIndex`);
if (index.html && index.js) {
Templates.replaceNode(this.element, index.html, index.js);
this.pendingContent.resolve();
return true;
}
return false;
}
/**
* Load the course index template.
*
* @param {Object} state the initial state
*/
async loadTemplateContent(state) {
// Collect section information from the state. // Collect section information from the state.
const exporter = this.reactive.getExporter(); const exporter = this.reactive.getExporter();
const data = exporter.course(state); const data = exporter.course(state);
try { try {
// To render an HTML into our component we just use the regular Templates module. // To render an HTML into our component we just use the regular Templates module.
const {html, js} = await Templates.renderForPromise( const {html, js} = await Templates.renderForPromise(
@ -62,7 +99,12 @@ export default class Component extends BaseComponent {
data, data,
); );
Templates.replaceNode(this.element, html, js); Templates.replaceNode(this.element, html, js);
this.pendingContent.resolve();
// Save the rendered template into the session cache.
this.reactive.setStorageValue(`courseIndex`, {html, js});
} catch (error) { } catch (error) {
this.pendingContent.resolve(error);
throw error; throw error;
} }
} }

View file

@ -25,10 +25,15 @@ Feature: Course index depending on role
| student1 | C1 | student | | student1 | C1 | student |
| teacher1 | C1 | editingteacher | | teacher1 | C1 | editingteacher |
@javascript
Scenario: Course index is present on course and activities. Scenario: Course index is present on course and activities.
Given I log in as "teacher1" Given I am on the "C1" "Course" page logged in as "teacher1"
When I am on "Course 1" course homepage When I click on "Side panel" "button"
Then I should see "Open course index drawer" Then I should see "Open course index drawer"
And I am on the "Activity sample 1" "assign activity" page
And I should see "Open course index drawer"
And I click on "Open course index drawer" "button"
And I should see "Activity sample 1" in the "courseindex-content" "region"
@javascript @javascript
Scenario: Course index as a teacher Scenario: Course index as a teacher

View file

@ -3867,15 +3867,17 @@ function core_course_core_calendar_get_valid_event_timestart_range(\calendar_eve
*/ */
function core_course_drawer(): string { function core_course_drawer(): string {
global $PAGE; global $PAGE;
// Only course are able to render course index.
if (!preg_match('/^(course).*/', $PAGE->pagetype)) {
return '';
}
$format = course_get_format($PAGE->course); $format = course_get_format($PAGE->course);
// Only course and modules are able to render course index.
$ismod = strpos($PAGE->pagetype, 'mod-') === 0;
if ($ismod || $PAGE->pagetype == 'course-view-' . $format->get_format()) {
$renderer = $format->get_renderer($PAGE); $renderer = $format->get_renderer($PAGE);
$placeholder = $renderer->course_index_drawer($format); $placeholder = $renderer->course_index_drawer($format);
return $placeholder; return $placeholder;
}
return '';
} }
/** /**

View file

@ -314,10 +314,15 @@ class page_requirements_manager {
// It is possible that the $page->context is null, so we can't use $page->context->id. // It is possible that the $page->context is null, so we can't use $page->context->id.
$contextid = null; $contextid = null;
$contextinstanceid = null;
if (!is_null($page->context)) { if (!is_null($page->context)) {
$contextid = $page->context->id; $contextid = $page->context->id;
$contextinstanceid = $page->context->instanceid;
} }
$courseid = $page->course->id;
$coursecontext = context_course::instance($courseid);
$this->M_cfg = array( $this->M_cfg = array(
'wwwroot' => $CFG->wwwroot, 'wwwroot' => $CFG->wwwroot,
'sesskey' => sesskey(), 'sesskey' => sesskey(),
@ -331,8 +336,10 @@ class page_requirements_manager {
'admin' => $CFG->admin, 'admin' => $CFG->admin,
'svgicons' => $page->theme->use_svg_icons(), 'svgicons' => $page->theme->use_svg_icons(),
'usertimezone' => usertimezone(), 'usertimezone' => usertimezone(),
'courseId' => (int) $page->course->id, 'courseId' => (int) $courseid,
'courseContextId' => $coursecontext->id,
'contextid' => $contextid, 'contextid' => $contextid,
'contextInstanceId' => (int) $contextinstanceid,
'langrev' => get_string_manager()->get_revision(), 'langrev' => get_string_manager()->get_revision(),
'templaterev' => $this->get_templaterev() 'templaterev' => $this->get_templaterev()
); );

View file

@ -1119,7 +1119,7 @@ class behat_navigation extends behat_base {
"//nav[@aria-label='Navigation bar']/ol/li[last()][contains(normalize-space(.), '" . $pagename . "')]" "//nav[@aria-label='Navigation bar']/ol/li[last()][contains(normalize-space(.), '" . $pagename . "')]"
); );
if (!$link) { if (!$link) {
$this->execute("behat_general::click_link", $pagename); $this->execute("behat_general::i_click_on_in_the", [$pagename, 'link', 'page', 'region']);
} }
} }

View file

@ -87,7 +87,7 @@ $bulkoperations = has_capability('moodle/course:bulkmessaging', $context);
$PAGE->set_title("$course->shortname: ".get_string('participants')); $PAGE->set_title("$course->shortname: ".get_string('participants'));
$PAGE->set_heading($course->fullname); $PAGE->set_heading($course->fullname);
$PAGE->set_pagetype('course-view-' . $course->format); $PAGE->set_pagetype('course-view-participants');
$PAGE->set_docs_path('enrol/users'); $PAGE->set_docs_path('enrol/users');
$PAGE->add_body_class('path-user'); // So we can style it independently. $PAGE->add_body_class('path-user'); // So we can style it independently.
$PAGE->set_other_editing_capability('moodle/course:manageactivities'); $PAGE->set_other_editing_capability('moodle/course:manageactivities');