Skip to content

Commit

Permalink
MDL-71209 courseformat: add state suport to legacy action
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ferranrecio authored and Amaia Anabitarte committed Aug 20, 2021
1 parent 830c3eb commit ef74500
Show file tree
Hide file tree
Showing 11 changed files with 342 additions and 15 deletions.
2 changes: 1 addition & 1 deletion course/amd/build/actions.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion course/amd/build/actions.min.js.map

Large diffs are not rendered by default.

156 changes: 151 additions & 5 deletions course/amd/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -234,13 +237,16 @@ 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'));
if (index === 0) {
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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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') {
Expand All @@ -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]');
Expand All @@ -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);
}
};

Expand Down Expand Up @@ -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.
Expand All @@ -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 */ {

/**
Expand Down Expand Up @@ -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),
Expand Down
10 changes: 10 additions & 0 deletions course/dndupload.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
},

/**
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions course/dnduploadlib.php
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,7 @@ protected function send_response($mod) {
$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);
Expand Down
116 changes: 116 additions & 0 deletions course/format/tests/behat/course_courseindex.feature
Original file line number Diff line number Diff line change
@@ -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"
Loading

0 comments on commit ef74500

Please sign in to comment.