Skip to content

Commit

Permalink
MDL-76894 core_courseformat: bulk move section
Browse files Browse the repository at this point in the history
  • Loading branch information
ferranrecio committed Mar 17, 2023
1 parent 602a308 commit 6ac6100
Show file tree
Hide file tree
Showing 18 changed files with 637 additions and 26 deletions.
2 changes: 1 addition & 1 deletion course/format/amd/build/local/content/actions.min.js

Large diffs are not rendered by default.

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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

36 changes: 23 additions & 13 deletions course/format/amd/src/local/content/actions.js
Expand Up @@ -211,11 +211,10 @@ export default class extends BaseComponent {
*/
async _requestMoveSection(target, event) {
// Check we have an id.
const sectionId = target.dataset.id;
if (!sectionId) {
const sectionIds = this._getTargetIds(target);
if (sectionIds.length == 0) {
return;
}
const sectionInfo = this.reactive.get('section', sectionId);

event.preventDefault();

Expand All @@ -225,27 +224,38 @@ export default class extends BaseComponent {
// Collect section information from the state.
const exporter = this.reactive.getExporter();
const data = exporter.course(this.reactive.state);
let titleText = null;

// Add the target section id and title.
data.sectionid = sectionInfo.id;
data.sectiontitle = sectionInfo.title;
let sectionInfo = null;
if (sectionIds.length == 1) {
sectionInfo = this.reactive.get('section', sectionIds[0]);
data.sectionid = sectionInfo.id;
data.sectiontitle = sectionInfo.title;
data.information = await this.reactive.getFormatString('sectionmove_info', data.sectiontitle);
titleText = this.reactive.getFormatString('sectionmove_title');
} else {
data.information = await this.reactive.getFormatString('sectionsmove_info', sectionIds.length);
titleText = this.reactive.getFormatString('sectionsmove_title');
}


// Build the modal parameters from the event data.
const modalParams = {
title: getString('movecoursesection', 'core'),
title: titleText,
body: Templates.render('core_courseformat/local/content/movesection', data),
};

// Create the modal.
const modal = await this._modalBodyRenderedPromise(modalParams);

const modalBody = getList(modal.getBody())[0];
const modalBody = getFirst(modal.getBody());

// Disable current element and section zero.
const currentElement = modalBody.querySelector(`${this.selectors.SECTIONLINK}[data-id='${sectionId}']`);
this._disableLink(currentElement);
const generalSection = modalBody.querySelector(`${this.selectors.SECTIONLINK}[data-number='0']`);
this._disableLink(generalSection);
// Disable current selected section ids.
sectionIds.forEach(sectionId => {
const currentElement = modalBody.querySelector(`${this.selectors.SECTIONLINK}[data-id='${sectionId}']`);
this._disableLink(currentElement);
});

// Setup keyboard navigation.
new ContentTree(
Expand All @@ -268,7 +278,7 @@ export default class extends BaseComponent {
return;
}
event.preventDefault();
this.reactive.dispatch('sectionMove', [sectionId], target.dataset.id);
this.reactive.dispatch('sectionMoveAfter', sectionIds, target.dataset.id);
this._destroyModal(modal, editTools);
});
}
Expand Down
20 changes: 20 additions & 0 deletions course/format/amd/src/local/courseeditor/mutations.js
Expand Up @@ -245,6 +245,26 @@ export default class {
const course = stateManager.get('course');
this.sectionLock(stateManager, sectionIds, true);
const updates = await this._callEditWebservice('section_move', course.id, sectionIds, targetSectionId);
this.bulkReset(stateManager);
stateManager.processUpdates(updates);
this.sectionLock(stateManager, sectionIds, false);
}

/**
* Move course modules after a specific course location.
*
* @param {StateManager} stateManager the current state manager
* @param {array} sectionIds the list of section ids to move
* @param {number} targetSectionId the target section id
*/
async sectionMoveAfter(stateManager, sectionIds, targetSectionId) {
if (!targetSectionId) {
throw new Error(`Mutation sectionMoveAfter requires targetSectionId`);
}
const course = stateManager.get('course');
this.sectionLock(stateManager, sectionIds, true);
const updates = await this._callEditWebservice('section_move_after', course.id, sectionIds, targetSectionId);
this.bulkReset(stateManager);
stateManager.processUpdates(updates);
this.sectionLock(stateManager, sectionIds, false);
}
Expand Down
31 changes: 31 additions & 0 deletions course/format/classes/base.php
Expand Up @@ -86,6 +86,8 @@ abstract class base {
private static $instances = array();
/** @var array plugin name => class name. */
private static $classesforformat = array('site' => 'site');
/** @var sectionmanager the format section manager. */
protected $sectionmanager = null;

/**
* Creates a new instance of class
Expand Down Expand Up @@ -810,6 +812,10 @@ public function get_editor_custom_strings(): array {
'sectiondelete_info',
'sectionsdelete_title',
'sectionsdelete_info',
'sectionmove_title',
'sectionmove_info',
'sectionsmove_title',
'sectionsmove_info',
'selectsection'
];
foreach ($formatoverridbles as $key) {
Expand Down Expand Up @@ -1546,6 +1552,31 @@ public function delete_module(cm_info $cm, bool $async = false) {
course_delete_module($cm->id, $async);
}

/**
* Moves a section just after the target section.
*
* @param section_info $section the section to move
* @param section_info $destination the section that should be below the moved section
* @return boolean if the section can be moved or not
*/
public function move_section_after(section_info $section, section_info $destination): bool {
if ($section->section == $destination->section || $section->section == $destination->section + 1) {
return true;
}
// The move_section_to moves relative to the section to move. However, this
// method will move the target section always after the destination.
if ($section->section > $destination->section) {
$newsectionnumber = $destination->section + 1;
} else {
$newsectionnumber = $destination->section;
}
return move_section_to(
$this->get_course(),
$section->section,
$newsectionnumber
);
}

/**
* Prepares the templateable object to display section name
*
Expand Down
10 changes: 10 additions & 0 deletions course/format/classes/output/local/content/bulkedittools.php
Expand Up @@ -137,6 +137,7 @@ protected function section_control_items(): array {
global $USER;
$format = $this->format;
$context = $format->get_context();
$sectionreturn = $format->get_section_number();
$user = $USER;

$controls = [];
Expand All @@ -151,6 +152,15 @@ protected function section_control_items(): array {
];
}

if (!$sectionreturn && has_capability('moodle/course:movesections', $context, $user)) {
$controls['move'] = [
'icon' => 'i/dragdrop',
'action' => 'moveSection',
'name' => get_string('move', 'moodle'),
'title' => $this->format->get_format_string('sectionsmove'),
'bulk' => 'section',
];
}

$deletecapabilities = ['moodle/course:movesections', 'moodle/course:update'];
if (has_all_capabilities($deletecapabilities, $context, $user)) {
Expand Down
85 changes: 85 additions & 0 deletions course/format/classes/stateactions.php
Expand Up @@ -164,6 +164,91 @@ public function section_move(
$updates->add_course_put();
}

/**
* Move course sections after to another location in the same course.
*
* @param stateupdates $updates the affected course elements track
* @param stdClass $course the course object
* @param int[] $ids the list of affected course module ids
* @param int $targetsectionid optional target section id
* @param int $targetcmid optional target cm id
*/
public function section_move_after(
stateupdates $updates,
stdClass $course,
array $ids,
?int $targetsectionid = null,
?int $targetcmid = null
): void {
// Validate target elements.
if (!$targetsectionid) {
throw new moodle_exception("Action section_move_after requires targetsectionid");
}

$this->validate_sections($course, $ids, __FUNCTION__);

$coursecontext = context_course::instance($course->id);
require_capability('moodle/course:movesections', $coursecontext);

// Section will move after the target section. This means it should be processed in
// descending order to keep the relative course order.
$this->validate_sections($course, [$targetsectionid], __FUNCTION__);
$ids = $this->sort_section_ids_by_section_number($course, $ids, true);

$format = course_get_format($course->id);
$affectedsections = [$targetsectionid => true];

foreach ($ids as $id) {
// An update section_info is needed as section numbers can change on every section movement.
$modinfo = get_fast_modinfo($course);
$section = $modinfo->get_section_info_by_id($id, MUST_EXIST);
$targetsection = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
$affectedsections[$section->id] = true;
$format->move_section_after($section, $targetsection);
}

// Use section_state to return the section and activities updated state.
$this->section_state($updates, $course, $ids, $targetsectionid);

// All course sections can be renamed because of the resort.
$modinfo = get_fast_modinfo($course);
$allsections = $modinfo->get_section_info_all();
foreach ($allsections as $section) {
// Ignore the affected sections because they are already in the updates.
if (isset($affectedsections[$section->id])) {
continue;
}
$updates->add_section_put($section->id);
}
// The section order is at a course level.
$updates->add_course_put();
}

/**
* Sort the sections ids depending on the section number.
*
* Some actions like move should be done in an specific order.
*
* @param stdClass $course the course object
* @param int[] $sectionids the array of section $ids
* @param bool $descending if the sort order must be descending instead of ascending
* @return int[] the array of section ids sorted by section number
*/
protected function sort_section_ids_by_section_number(
stdClass $course,
array $sectionids,
bool $descending = false
): array {
$sorting = ($descending) ? -1 : 1;
$sortfunction = function ($asection, $bsection) use ($sorting) {
return ($asection->section <=> $bsection->section) * $sorting;
};
$modinfo = get_fast_modinfo($course);
$sections = $this->get_section_info($modinfo, $sectionids);
uasort($sections, $sortfunction);
return array_keys($sections);
}

/**
* Create a course section.
*
Expand Down
6 changes: 5 additions & 1 deletion course/format/templates/local/content/movesection.mustache
Expand Up @@ -42,7 +42,11 @@
}

}}
<p data-for="sectionname">{{#str}} movefull, moodle, {{sectiontitle}} {{/str}}:</p>
{{#information}}
<p data-for="sectionname">
{{information}}:
</p>
{{/information}}
<nav class="collapse-list" id="destination-selector" role="tree">
{{#sections}}
<div
Expand Down

0 comments on commit 6ac6100

Please sign in to comment.