MDL-71209 courseformat: add state suport to legacy action

Adapt the current course editing libraries to modify also
the course state data. This way, any UI component that
watches the course structure can react to the changes.
This commit is contained in:
Ferran Recio 2021-06-21 13:13:02 +02:00 committed by Amaia Anabitarte
parent 830c3eb907
commit ef745009ed
11 changed files with 342 additions and 15 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

@ -22,8 +22,11 @@
* @since 3.3
*/
define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/url', 'core/yui',
'core/modal_factory', 'core/modal_events', 'core/key_codes', 'core/log'],
function($, ajax, templates, notification, str, url, Y, ModalFactory, ModalEvents, KeyCodes, log) {
'core/modal_factory', 'core/modal_events', 'core/key_codes', 'core/log', 'core_courseformat/courseeditor'],
function($, ajax, templates, notification, str, url, Y, ModalFactory, ModalEvents, KeyCodes, log, editor) {
const courseeditor = editor.getCurrentCourseEditor();
var CSS = {
EDITINPROGRESS: 'editinprogress',
SECTIONDRAGGABLE: 'sectiondraggable',
@ -234,6 +237,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
.done(function(data) {
var elementToFocus = findNextFocusable(moduleElement);
moduleElement.replaceWith(data);
let affectedids = [];
// Initialise action menu for activity(ies) added as a result of this.
$('<div>' + data + '</div>').find(SELECTOR.ACTIVITYLI).each(function(index) {
initActionMenu($(this).attr('id'));
@ -241,6 +245,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
focusActionItem($(this).attr('id'), action);
elementToFocus = null;
}
// Save any activity id in cmids.
affectedids.push(getModuleId($(this)));
});
// In case of activity deletion focus the next focusable element.
if (elementToFocus) {
@ -251,6 +257,10 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
removeLightbox(lightbox, 400);
// Trigger event that can be observed by course formats.
moduleElement.trigger($.Event('coursemoduleedited', {ajaxreturn: data, action: action}));
// Modify cm state.
courseeditor.dispatch('legacyActivityAction', action, cmid, affectedids);
}).fail(function(ex) {
// Remove spinner and lightbox.
removeSpinner(moduleElement, spinner);
@ -377,8 +387,9 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
* @param {JQuery} actionItem
* @param {Object} data
* @param {String} courseformat
* @param {Number} sectionid
*/
var defaultEditSectionHandler = function(sectionElement, actionItem, data, courseformat) {
var defaultEditSectionHandler = function(sectionElement, actionItem, data, courseformat, sectionid) {
var action = actionItem.attr('data-action');
if (action === 'hide' || action === 'show') {
if (action === 'hide') {
@ -400,6 +411,11 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
if (data.section_availability !== undefined) {
sectionElement.find('.section_availability').first().replaceWith(data.section_availability);
}
// Modify course state.
const section = courseeditor.state.section.get(sectionid);
if (section !== undefined) {
courseeditor.dispatch('sectionState', [sectionid]);
}
} else if (action === 'setmarker') {
var oldmarker = $(SELECTOR.SECTIONLI + '.current'),
oldActionItem = oldmarker.find(SELECTOR.SECTIONACTIONMENU + ' ' + 'a[data-action=removemarker]');
@ -409,10 +425,12 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
sectionElement.addClass('current');
replaceActionItem(actionItem, 'i/marked',
'highlightoff', 'core', 'removemarker');
courseeditor.dispatch('legacySectionAction', action, sectionid);
} else if (action === 'removemarker') {
sectionElement.removeClass('current');
replaceActionItem(actionItem, 'i/marker',
'highlight', 'core', 'setmarker');
courseeditor.dispatch('legacySectionAction', action, sectionid);
}
};
@ -460,7 +478,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
var e = $.Event('coursesectionedited', {ajaxreturn: data, action: action});
sectionElement.trigger(e);
if (!e.isDefaultPrevented()) {
defaultEditSectionHandler(sectionElement, target, data, courseformat);
defaultEditSectionHandler(sectionElement, target, data, courseformat, sectionid);
}
}).fail(function(ex) {
// Remove spinner and lightbox.
@ -487,10 +505,121 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
var sectionreturn = mainelement.find('.' + CSS.EDITINGMOVE).attr('data-sectionreturn');
refreshModule(mainelement, cmid, sectionreturn);
}
}
},
/**
* Update the course state when some cm is moved via YUI.
* @param {*} params
*/
updateMovedCmState: (params) => {
const state = courseeditor.state;
// Update old section.
const cm = state.cm.get(params.cmid);
if (cm !== undefined) {
courseeditor.dispatch('sectionState', [cm.sectionid]);
}
// Update cm state.
courseeditor.dispatch('cmState', [params.cmid]);
},
/**
* Update the course state when some section is moved via YUI.
*/
updateMovedSectionState: () => {
courseeditor.dispatch('courseState');
},
});
});
// From Moodle 4.0 all edit actions are being re-implemented as state mutation.
// This means all method from this "actions" module will be deprecated when all the course
// interface is migrated to reactive components.
// Most legacy actions did not provide enough information to regenarate the course so they
// use the mutations courseState, sectionState and cmState to get the updated state from
// the server. However, some activity actions where we can prevent an extra webservice
// call by implementing an adhoc mutation.
courseeditor.addMutations({
/**
* Compatibility function to update Moodle 4.0 course state using legacy actions.
*
* This method only updates some actions which does not require to use cmState mutation
* to get updated data form the server.
*
* @param {Object} statemanager the current state in read write mode
* @param {String} action the performed action
* @param {Number} cmid the affected course module id
* @param {Array} affectedids all affected cm ids (for duplicate action)
*/
legacyActivityAction: function(statemanager, action, cmid, affectedids) {
const state = statemanager.state;
const cm = state.cm.get(cmid);
if (cm === undefined) {
return;
}
const section = state.section.get(cm.sectionid);
if (section === undefined) {
return;
}
statemanager.setReadOnly(false);
switch (action) {
case 'delete':
// Remove from section.
section.cmlist = section.cmlist.reduce(
(cmlist, current) => {
if (current != cmid) {
cmlist.push(current);
}
return cmlist;
},
[]
);
// Delete form list.
state.cm.delete(cmid);
break;
case 'hide':
case 'show':
cm.visible = (action === 'show') ? true : false;
break;
case 'duplicate':
// Duplicate requires to get extra data from the server.
courseeditor.dispatch('cmState', affectedids);
break;
}
statemanager.setReadOnly(true);
},
legacySectionAction: function(statemanager, action, sectionid) {
const state = statemanager.state;
const section = state.section.get(sectionid);
if (section === undefined) {
return;
}
statemanager.setReadOnly(false);
switch (action) {
case 'setmarker':
// Remove previous marker.
state.section.forEach((current) => {
if (current.id != sectionid) {
current.current = false;
}
});
section.current = true;
break;
case 'removemarker':
section.current = false;
break;
}
statemanager.setReadOnly(true);
},
});
return /** @alias module:core_course/actions */ {
/**
@ -562,6 +691,23 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
}
});
// The section and activity names are edited using inplace editable.
// The "update" jQuery event must be captured in order to update the course state.
$('body').on('updated', `${SELECTOR.SECTIONLI} [data-inplaceeditable]`, function(e) {
if (e.ajaxreturn && e.ajaxreturn.itemid) {
const state = courseeditor.state;
const section = state.section.get(e.ajaxreturn.itemid);
if (section !== undefined) {
courseeditor.dispatch('sectionState', [e.ajaxreturn.itemid]);
}
}
});
$('body').on('updated', `${SELECTOR.ACTIVITYLI} [data-inplaceeditable]`, function(e) {
if (e.ajaxreturn && e.ajaxreturn.itemid) {
courseeditor.dispatch('cmState', [e.ajaxreturn.itemid]);
}
});
// Add a handler for "Add sections" link to ask for a number of sections to add.
str.get_string('numberweeks').done(function(strNumberSections) {
var trigger = $(SELECTOR.ADDSECTIONS),

View File

@ -95,6 +95,12 @@ M.course_dndupload = {
if (options.showstatus) {
this.add_status_div();
}
// Any change to the course must be applied also to the course state via the courseeditor module.
var self = this;
require(['core_courseformat/courseeditor'], function(editor) {
self.courseeditor = editor.getCurrentCourseEditor();
});
},
/**
@ -781,6 +787,8 @@ M.course_dndupload = {
resel.li.outerHTML = unescape(resel.li.outerHTML);
}
self.add_editing(result.elementid);
// Once done, send any new course module id to the courseeditor to update de course state.
self.courseeditor.dispatch('cmState', [result.cmid]);
// Fire the content updated event.
require(['core/event', 'jquery'], function(event, $) {
event.notifyFilterContentUpdated($(result.fullcontent));
@ -1047,6 +1055,8 @@ M.course_dndupload = {
resel.li.outerHTML = unescape(resel.li.outerHTML);
}
self.add_editing(result.elementid);
// Once done, send any new course module id to the courseeditor to update de course state.
self.courseeditor.dispatch('cmState', [result.cmid]);
} else {
// Error - remove the dummy element
resel.parent.removeChild(resel.li);

View File

@ -651,6 +651,7 @@ class dndupload_ajax_processor {
$resp = new stdClass();
$resp->error = self::ERROR_OK;
$resp->elementid = 'module-' . $mod->id;
$resp->cmid = $mod->id;
$format = course_get_format($this->course);
$renderer = $format->get_renderer($PAGE);

View File

@ -0,0 +1,116 @@
@core @core_course
Feature: Course index depending on role
In order to quickly access the course structure
As a user
I need to see the current course structure in the course index.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| enablecompletion | 1 |
| numsections | 4 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 |
| book | Activity sample 2 | Test book description | C1 | sample2 | 2 |
| choice | Activity sample 3 | Test choice description | C1 | sample3 | 3 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| teacher1 | C1 | editingteacher |
Scenario: Course index is present on course and activities.
Given I log in as "teacher1"
When I am on "Course 1" course homepage
Then I should see "Open course index drawer"
And I follow "Activity sample 1"
And I should see "Open course index drawer"
@javascript
Scenario: Course index as a teacher
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I click on "Side panel" "button"
When I click on "Open course index drawer" "button"
And I click on "Topic 1" "link" in the "courseindex-content" "region"
And I click on "Topic 2" "link" in the "courseindex-content" "region"
And I click on "Topic 3" "link" in the "courseindex-content" "region"
Then I should see "Topic 1" in the "courseindex-content" "region"
And I should see "Topic 2" in the "courseindex-content" "region"
And I should see "Topic 3" in the "courseindex-content" "region"
And I should see "Activity sample 1" in the "courseindex-content" "region"
And I should see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Activity sample 3" in the "courseindex-content" "region"
@javascript
Scenario: Teacher can see hiden activities and sections
Given I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And I hide section "2"
And I open "Activity sample 3" actions menu
And I click on "Hide" "link" in the "Activity sample 3" activity
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I click on "Side panel" "button"
When I click on "Open course index drawer" "button"
And I click on "Topic 1" "link" in the "courseindex-content" "region"
And I click on "Topic 2" "link" in the "courseindex-content" "region"
And I click on "Topic 3" "link" in the "courseindex-content" "region"
Then I should see "Topic 1" in the "courseindex-content" "region"
And I should see "Topic 2" in the "courseindex-content" "region"
And I should see "Topic 3" in the "courseindex-content" "region"
And I should see "Activity sample 1" in the "courseindex-content" "region"
And I should see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Activity sample 3" in the "courseindex-content" "region"
@javascript
Scenario: Students can only see visible activies and sections
Given I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And I hide section "2"
And I open "Activity sample 3" actions menu
And I click on "Hide" "link" in the "Activity sample 3" activity
And I log out
And I log in as "student1"
And I am on "Course 1" course homepage
And I click on "Side panel" "button"
When I click on "Open course index drawer" "button"
And I click on "Topic 1" "link" in the "courseindex-content" "region"
And I click on "Topic 3" "link" in the "courseindex-content" "region"
Then I should see "Topic 1" in the "courseindex-content" "region"
And I should not see "Topic 2" in the "courseindex-content" "region"
And I should see "Topic 3" in the "courseindex-content" "region"
And I should see "Activity sample 1" in the "courseindex-content" "region"
And I should not see "Activity sample 2" in the "courseindex-content" "region"
And I should not see "Activity sample 3" in the "courseindex-content" "region"
@javascript
Scenario: Delete an activity as a teacher
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I click on "Side panel" "button"
When I delete "Activity sample 2" activity
And I click on "Open course index drawer" "button"
And I click on "Topic 1" "link" in the "courseindex-content" "region"
And I click on "Topic 2" "link" in the "courseindex-content" "region"
Then I should not see "Activity sample 2" in the "courseindex-content" "region"
@javascript
Scenario: Highlight sections are represented in the course index.
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I click on "Side panel" "button"
And I turn section "2" highlighting on
# Current section is only marked visually in the course index.
And the "class" attribute of "#courseindex-content [data-for='section'][data-number='2']" "css_element" should contain "current"
When I turn section "1" highlighting on
And I click on "Open course index drawer" "button"
# Current section is only marked visually in the course index.
Then the "class" attribute of "#courseindex-content [data-for='section'][data-number='1']" "css_element" should contain "current"

View File

@ -295,6 +295,9 @@ Y.extend(DRAGSECTION, M.core.dragdrop, {
window.setTimeout(function() {
lightbox.hide();
}, 250);
// Update course state.
M.course.coursebase.invoke_function('updateMovedSectionState');
},
failure: function(tid, response) {
@ -478,16 +481,21 @@ Y.extend(DRAGRESOURCE, M.core.dragdrop, {
params[varname] = pageparams[varname];
}
// Variables needed to update the course state.
var cmid = Number(Y.Moodle.core_course.util.cm.getId(dragnode));
var beforeid = null;
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseId = this.get('courseid');
params['class'] = 'resource';
params.field = 'move';
params.id = Number(Y.Moodle.core_course.util.cm.getId(dragnode));
params.id = cmid;
params.sectionId = Y.Moodle.core_course.util.section.getId(dropnode.ancestor(M.course.format.get_section_wrapper(Y), true));
if (dragnode.next()) {
params.beforeId = Number(Y.Moodle.core_course.util.cm.getId(dragnode.next()));
beforeid = Number(Y.Moodle.core_course.util.cm.getId(dragnode.next()));
params.beforeId = beforeid;
}
// Do AJAX request
@ -503,6 +511,16 @@ Y.extend(DRAGRESOURCE, M.core.dragdrop, {
},
success: function(tid, response) {
var responsetext = Y.JSON.parse(response.responseText);
// Update course state.
M.course.coursebase.invoke_function(
'updateMovedCmState',
{
cmid: cmid,
beforeid: beforeid,
visible: responsetext.visible,
}
);
// Set visibility in course content.
var params = {element: dragnode, visible: responsetext.visible};
M.course.coursebase.invoke_function('set_visibility_resource_ui', params);
this.unlock_drag_handle(drag, CSS.EDITINGMOVE);

File diff suppressed because one or more lines are too long

View File

@ -291,6 +291,9 @@ Y.extend(DRAGSECTION, M.core.dragdrop, {
window.setTimeout(function() {
lightbox.hide();
}, 250);
// Update course state.
M.course.coursebase.invoke_function('updateMovedSectionState');
},
failure: function(tid, response) {
@ -474,16 +477,21 @@ Y.extend(DRAGRESOURCE, M.core.dragdrop, {
params[varname] = pageparams[varname];
}
// Variables needed to update the course state.
var cmid = Number(Y.Moodle.core_course.util.cm.getId(dragnode));
var beforeid = null;
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseId = this.get('courseid');
params['class'] = 'resource';
params.field = 'move';
params.id = Number(Y.Moodle.core_course.util.cm.getId(dragnode));
params.id = cmid;
params.sectionId = Y.Moodle.core_course.util.section.getId(dropnode.ancestor(M.course.format.get_section_wrapper(Y), true));
if (dragnode.next()) {
params.beforeId = Number(Y.Moodle.core_course.util.cm.getId(dragnode.next()));
beforeid = Number(Y.Moodle.core_course.util.cm.getId(dragnode.next()));
params.beforeId = beforeid;
}
// Do AJAX request
@ -499,6 +507,16 @@ Y.extend(DRAGRESOURCE, M.core.dragdrop, {
},
success: function(tid, response) {
var responsetext = Y.JSON.parse(response.responseText);
// Update course state.
M.course.coursebase.invoke_function(
'updateMovedCmState',
{
cmid: cmid,
beforeid: beforeid,
visible: responsetext.visible,
}
);
// Set visibility in course content.
var params = {element: dragnode, visible: responsetext.visible};
M.course.coursebase.invoke_function('set_visibility_resource_ui', params);
this.unlock_drag_handle(drag, CSS.EDITINGMOVE);

View File

@ -151,16 +151,21 @@ Y.extend(DRAGRESOURCE, M.core.dragdrop, {
params[varname] = pageparams[varname];
}
// Variables needed to update the course state.
var cmid = Number(Y.Moodle.core_course.util.cm.getId(dragnode));
var beforeid = null;
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseId = this.get('courseid');
params['class'] = 'resource';
params.field = 'move';
params.id = Number(Y.Moodle.core_course.util.cm.getId(dragnode));
params.id = cmid;
params.sectionId = Y.Moodle.core_course.util.section.getId(dropnode.ancestor(M.course.format.get_section_wrapper(Y), true));
if (dragnode.next()) {
params.beforeId = Number(Y.Moodle.core_course.util.cm.getId(dragnode.next()));
beforeid = Number(Y.Moodle.core_course.util.cm.getId(dragnode.next()));
params.beforeId = beforeid;
}
// Do AJAX request
@ -176,6 +181,16 @@ Y.extend(DRAGRESOURCE, M.core.dragdrop, {
},
success: function(tid, response) {
var responsetext = Y.JSON.parse(response.responseText);
// Update course state.
M.course.coursebase.invoke_function(
'updateMovedCmState',
{
cmid: cmid,
beforeid: beforeid,
visible: responsetext.visible,
}
);
// Set visibility in course content.
var params = {element: dragnode, visible: responsetext.visible};
M.course.coursebase.invoke_function('set_visibility_resource_ui', params);
this.unlock_drag_handle(drag, CSS.EDITINGMOVE);

View File

@ -263,6 +263,9 @@ Y.extend(DRAGSECTION, M.core.dragdrop, {
window.setTimeout(function() {
lightbox.hide();
}, 250);
// Update course state.
M.course.coursebase.invoke_function('updateMovedSectionState');
},
failure: function(tid, response) {