MDL-83469 core_courseformat: alternative new_module actions
The previous create_module actions uses section number instead of section id. This produces several bugs when sections are moved around the course in edit mode. This issue implements a new_module action with all related mutations and webservices. The new one uses section id instead of section number.
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
issueNumber: MDL-83469
|
||||
notes:
|
||||
core_courseformat:
|
||||
- message: >-
|
||||
The core_courseformat_create_module web service has been deprecated.
|
||||
Please use core_courseformat_new_module as its replacement.
|
||||
type: deprecated
|
||||
- message: >-
|
||||
The state mutation addModule, primarily used for creating mod_subsection
|
||||
instances, has been deprecated. Replace it with newModule. Additionally,
|
||||
all course formats using links with data-action="addModule" must be updated
|
||||
to use data-action="newModule" and include a data-sectionid attribute
|
||||
specifying the target section ID.
|
||||
type: deprecated
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -68,7 +68,7 @@ define(
|
||||
const componentActions = [
|
||||
'moveSection', 'moveCm', 'addSection', 'deleteSection', 'cmDelete', 'cmDuplicate', 'sectionHide', 'sectionShow',
|
||||
'cmHide', 'cmShow', 'cmStealth', 'sectionHighlight', 'sectionUnhighlight', 'cmMoveRight', 'cmMoveLeft',
|
||||
'cmNoGroups', 'cmVisibleGroups', 'cmSeparateGroups', 'addModule',
|
||||
'cmNoGroups', 'cmVisibleGroups', 'cmSeparateGroups', 'addModule', 'newModule',
|
||||
];
|
||||
|
||||
// The course reactive instance.
|
||||
|
||||
+2
-2
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,7 @@
|
||||
import {BaseComponent} from 'core/reactive';
|
||||
import {eventTypes} from 'core/local/inplace_editable/events';
|
||||
import Collapse from 'theme_boost/bootstrap/collapse';
|
||||
import log from 'core/log';
|
||||
import Modal from 'core/modal';
|
||||
import ModalSaveCancel from 'core/modal_save_cancel';
|
||||
import ModalDeleteCancel from 'core/modal_delete_cancel';
|
||||
@@ -488,14 +489,28 @@ export default class extends BaseComponent {
|
||||
/**
|
||||
* Handle a create subsection request.
|
||||
*
|
||||
* @deprecated since Moodle 5.0 MDL-83469.
|
||||
* @todo MDL-83851 This will be deleted in Moodle 6.0.
|
||||
* @param {Element} target the dispatch action element
|
||||
* @param {Event} event the triggered event
|
||||
*/
|
||||
async _requestAddModule(target, event) {
|
||||
log.debug('AddModule action is deprecated. Use newModule instead');
|
||||
event.preventDefault();
|
||||
this.reactive.dispatch('addModule', target.dataset.modname, target.dataset.sectionnum, target.dataset.beforemod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a new create subsection request.
|
||||
*
|
||||
* @param {Element} target the dispatch action element
|
||||
* @param {Event} event the triggered event
|
||||
*/
|
||||
async _requestNewModule(target, event) {
|
||||
event.preventDefault();
|
||||
this.reactive.dispatch('newModule', target.dataset.modname, target.dataset.sectionid, target.dataset.beforemod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a delete section request.
|
||||
*
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
import ajax from 'core/ajax';
|
||||
import {getString} from "core/str";
|
||||
import log from 'core/log';
|
||||
import SRLogger from "core/local/reactive/srlogger";
|
||||
|
||||
/**
|
||||
@@ -68,27 +69,55 @@ export default class {
|
||||
/**
|
||||
* Private method to call core_courseformat_create_module webservice.
|
||||
*
|
||||
* @deprecated since Moodle 5.0 MDL-83469.
|
||||
* @todo MDL-83851 This will be deleted in Moodle 6.0.
|
||||
* @method _callEditWebservice
|
||||
* @param {number} courseId
|
||||
* @param {string} modName module name
|
||||
* @param {number} targetSectionNum target section number
|
||||
* @param {number} targetCmId optional target cm id
|
||||
*/
|
||||
async _callAddModuleWebservice(courseId, modName, targetSectionNum, targetCmId) {
|
||||
const args = {
|
||||
courseid: courseId,
|
||||
modname: modName,
|
||||
targetsectionnum: targetSectionNum,
|
||||
};
|
||||
if (targetCmId) {
|
||||
args.targetcmid = targetCmId;
|
||||
}
|
||||
let ajaxresult = await ajax.call([{
|
||||
methodname: 'core_courseformat_create_module',
|
||||
args,
|
||||
}])[0];
|
||||
return JSON.parse(ajaxresult);
|
||||
async _callAddModuleWebservice(courseId, modName, targetSectionNum, targetCmId) {
|
||||
log.debug('_callAddModuleWebservice() is deprecated. Use _callNewModuleWebservice() instead');
|
||||
const args = {
|
||||
courseid: courseId,
|
||||
modname: modName,
|
||||
targetsectionnum: targetSectionNum,
|
||||
};
|
||||
if (targetCmId) {
|
||||
args.targetcmid = targetCmId;
|
||||
}
|
||||
let ajaxresult = await ajax.call([{
|
||||
methodname: 'core_courseformat_create_module',
|
||||
args,
|
||||
}])[0];
|
||||
return JSON.parse(ajaxresult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to call core_courseformat_new_module webservice.
|
||||
*
|
||||
* @method _callEditWebservice
|
||||
* @param {number} courseId
|
||||
* @param {string} modName module name
|
||||
* @param {number} targetSectionId target section number
|
||||
* @param {number} targetCmId optional target cm id
|
||||
*/
|
||||
async _callNewModuleWebservice(courseId, modName, targetSectionId, targetCmId) {
|
||||
const args = {
|
||||
courseid: courseId,
|
||||
modname: modName,
|
||||
targetsectionid: targetSectionId,
|
||||
};
|
||||
if (targetCmId) {
|
||||
args.targetcmid = targetCmId;
|
||||
}
|
||||
let ajaxresult = await ajax.call([{
|
||||
methodname: 'core_courseformat_new_module',
|
||||
args,
|
||||
}])[0];
|
||||
return JSON.parse(ajaxresult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a basic section state action.
|
||||
@@ -396,12 +425,15 @@ export default class {
|
||||
/**
|
||||
* Add a new module to a specific course section.
|
||||
*
|
||||
* @deprecated since Moodle 5.0 MDL-83469.
|
||||
* @todo MDL-83851 This will be deleted in Moodle 6.0.
|
||||
* @param {StateManager} stateManager the current state manager
|
||||
* @param {string} modName the modulename to add
|
||||
* @param {number} targetSectionNum the target section number
|
||||
* @param {number} targetCmId optional the target cm id
|
||||
*/
|
||||
async addModule(stateManager, modName, targetSectionNum, targetCmId) {
|
||||
log.debug('addModule() is deprecated. Use newModule() instead');
|
||||
if (!modName) {
|
||||
throw new Error(`Mutation addModule requires moduleName`);
|
||||
}
|
||||
@@ -416,6 +448,29 @@ export default class {
|
||||
stateManager.processUpdates(updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new module to a specific course section.
|
||||
*
|
||||
* @param {StateManager} stateManager the current state manager
|
||||
* @param {string} modName the modulename to add
|
||||
* @param {number} targetSectionId the target section id
|
||||
* @param {number} targetCmId optional the target cm id
|
||||
*/
|
||||
async newModule(stateManager, modName, targetSectionId, targetCmId) {
|
||||
if (!modName) {
|
||||
throw new Error(`Mutation newModule requires moduleName`);
|
||||
}
|
||||
if (!targetSectionId) {
|
||||
throw new Error(`Mutation newModule requires targetSectionId`);
|
||||
}
|
||||
if (!targetCmId) {
|
||||
targetCmId = 0;
|
||||
}
|
||||
const course = stateManager.get('course');
|
||||
const updates = await this._callNewModuleWebservice(course.id, modName, targetSectionId, targetCmId);
|
||||
stateManager.processUpdates(updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark or unmark course modules as dragging.
|
||||
*
|
||||
|
||||
@@ -30,12 +30,15 @@ use core_courseformat\base as course_format;
|
||||
* @package core_courseformat
|
||||
* @copyright 2024 Mikel Martín <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @deprecated since Moodle 5.0
|
||||
* @todo Remove this class in Moodle 6.0 MDL-83851
|
||||
*/
|
||||
class create_module extends external_api {
|
||||
|
||||
/**
|
||||
* Webservice parameters.
|
||||
*
|
||||
* @deprecated since Moodle 5.0
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function execute_parameters(): external_function_parameters {
|
||||
@@ -60,6 +63,7 @@ class create_module extends external_api {
|
||||
* on cms, sections and the general course data. However, if some plugin needs adhoc messages for
|
||||
* its own mutation module, extend this class in format_XXX\course.
|
||||
*
|
||||
* @deprecated since Moodle 5.0
|
||||
* @param int $courseid the course id
|
||||
* @param string $modname the module name
|
||||
* @param int $targetsectionnum the target section number
|
||||
@@ -139,9 +143,18 @@ class create_module extends external_api {
|
||||
/**
|
||||
* Webservice returns.
|
||||
*
|
||||
* @deprecated since Moodle 5.0
|
||||
* @return external_value
|
||||
*/
|
||||
public static function execute_returns(): external_value {
|
||||
return new external_value(PARAM_RAW, 'Encoded course update JSON');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the function as deprecated.
|
||||
* @return bool
|
||||
*/
|
||||
public static function execute_is_deprecated() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core_courseformat\external;
|
||||
|
||||
use core_external\external_api;
|
||||
use core_external\external_function_parameters;
|
||||
use core_external\external_value;
|
||||
use moodle_exception;
|
||||
use coding_exception;
|
||||
use context_course;
|
||||
use core_courseformat\base as course_format;
|
||||
|
||||
/**
|
||||
* External service to create a new module instance in the course.
|
||||
*
|
||||
* @package core_courseformat
|
||||
* @copyright 2024 Mikel Martín <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class new_module extends external_api {
|
||||
|
||||
/**
|
||||
* Webservice parameters.
|
||||
*
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function execute_parameters(): external_function_parameters {
|
||||
return new external_function_parameters(
|
||||
[
|
||||
'courseid' => new external_value(PARAM_INT, 'course id', VALUE_REQUIRED),
|
||||
'modname' => new external_value(PARAM_ALPHANUMEXT, 'module name', VALUE_REQUIRED),
|
||||
'targetsectionid' => new external_value(PARAM_INT, 'target section id', VALUE_REQUIRED, null),
|
||||
'targetcmid' => new external_value(PARAM_INT, 'Optional target cm id', VALUE_DEFAULT, null),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This webservice will execute the create_module action from the course editor.
|
||||
*
|
||||
* The action will register in a {@see \core_courseformat\stateupdates} all the affected
|
||||
* sections, cms and course attribute. This object (in JSON) will be sent back to the
|
||||
* frontend editor to refresh the updated state elements.
|
||||
*
|
||||
* By default, {@see \core_courseformat\stateupdates} will register only create, delete and update events
|
||||
* on cms, sections and the general course data. However, if some plugin needs adhoc messages for
|
||||
* its own mutation module, extend this class in format_XXX\course.
|
||||
*
|
||||
* @param int $courseid the course id
|
||||
* @param string $modname the module name
|
||||
* @param int $targetsectionid the target section id
|
||||
* @param int|null $targetcmid optional target cm id
|
||||
* @return string Course state in JSON
|
||||
*/
|
||||
public static function execute(
|
||||
int $courseid,
|
||||
string $modname,
|
||||
int $targetsectionid,
|
||||
?int $targetcmid = null
|
||||
): string {
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/course/lib.php');
|
||||
|
||||
[
|
||||
'courseid' => $courseid,
|
||||
'modname' => $modname,
|
||||
'targetsectionid' => $targetsectionid,
|
||||
'targetcmid' => $targetcmid,
|
||||
] = self::validate_parameters(self::execute_parameters(), [
|
||||
'courseid' => $courseid,
|
||||
'modname' => $modname,
|
||||
'targetsectionid' => $targetsectionid,
|
||||
'targetcmid' => $targetcmid,
|
||||
]);
|
||||
|
||||
self::validate_context(context_course::instance($courseid));
|
||||
|
||||
// Plugin needs to support quick creation and the course format needs to support components.
|
||||
// Formats using YUI modules should not be able to quick-create because the front end cannot react to the change.
|
||||
if (!plugin_supports('mod', $modname, FEATURE_QUICKCREATE) || !course_get_format($courseid)->supports_components()) {
|
||||
throw new moodle_exception("Module $modname does not support quick creation");
|
||||
}
|
||||
|
||||
$courseformat = course_get_format($courseid);
|
||||
|
||||
// Create a course changes tracker object.
|
||||
$defaultupdatesclass = 'core_courseformat\\stateupdates';
|
||||
$updatesclass = 'format_' . $courseformat->get_format() . '\\courseformat\\stateupdates';
|
||||
if (!class_exists($updatesclass)) {
|
||||
$updatesclass = $defaultupdatesclass;
|
||||
}
|
||||
$updates = new $updatesclass($courseformat);
|
||||
|
||||
if (!is_a($updates, $defaultupdatesclass)) {
|
||||
throw new coding_exception("The \"$updatesclass\" class must extend \"$defaultupdatesclass\"");
|
||||
}
|
||||
|
||||
// Get the actions class from the course format.
|
||||
$actionsclass = 'format_'. $courseformat->get_format().'\\courseformat\\stateactions';
|
||||
if (!class_exists($actionsclass)) {
|
||||
$actionsclass = 'core_courseformat\\stateactions';
|
||||
}
|
||||
/** @var \core_courseformat\stateactions $actions */
|
||||
$actions = new $actionsclass();
|
||||
|
||||
$action = 'new_module';
|
||||
if (!is_callable([$actions, $action])) {
|
||||
throw new moodle_exception("Invalid course state action $action in ".get_class($actions));
|
||||
}
|
||||
|
||||
$course = $courseformat->get_course();
|
||||
|
||||
// Execute the action.
|
||||
$actions->$action($updates, $course, $modname, $targetsectionid, $targetcmid);
|
||||
|
||||
// Dispatch the hook for post course content update.
|
||||
$hook = new \core_courseformat\hook\after_course_content_updated(
|
||||
course: $course
|
||||
);
|
||||
\core\di::get(\core\hook\manager::class)->dispatch($hook);
|
||||
|
||||
return json_encode($updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Webservice returns.
|
||||
*
|
||||
* @return external_value
|
||||
*/
|
||||
public static function execute_returns(): external_value {
|
||||
return new external_value(PARAM_RAW, 'Encoded course update JSON');
|
||||
}
|
||||
}
|
||||
@@ -1135,12 +1135,19 @@ class stateactions {
|
||||
/**
|
||||
* Create a course module.
|
||||
*
|
||||
* @deprecated since Moodle 5.0, use new_module instead.
|
||||
* @todo MDL-83851: final deprecation of this method in Moodle 6.0.
|
||||
* @param stateupdates $updates the affected course elements track
|
||||
* @param stdClass $course the course object
|
||||
* @param string $modname the module name
|
||||
* @param int $targetsectionnum target section number
|
||||
* @param int|null $targetcmid optional target cm id
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
replacement: 'new_module',
|
||||
since: '5.0',
|
||||
mdl: 'MDL-83469',
|
||||
)]
|
||||
public function create_module(
|
||||
stateupdates $updates,
|
||||
stdClass $course,
|
||||
@@ -1151,6 +1158,8 @@ class stateactions {
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/course/modlib.php');
|
||||
|
||||
\core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
|
||||
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
require_capability('moodle/course:update', $coursecontext);
|
||||
|
||||
@@ -1162,4 +1171,38 @@ class stateactions {
|
||||
// Adding module affects section structure, and if the module has a delegated section even the course structure.
|
||||
$this->course_state($updates, $course);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new course module.
|
||||
*
|
||||
* @param stateupdates $updates the affected course elements track
|
||||
* @param stdClass $course the course object
|
||||
* @param string $modname the module name
|
||||
* @param int $targetsectionid target section id
|
||||
* @param int|null $targetcmid optional target cm id
|
||||
*/
|
||||
public function new_module(
|
||||
stateupdates $updates,
|
||||
stdClass $course,
|
||||
string $modname,
|
||||
int $targetsectionid,
|
||||
?int $targetcmid = null
|
||||
): void {
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/course/modlib.php');
|
||||
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
require_capability('moodle/course:update', $coursecontext);
|
||||
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
$section = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
|
||||
|
||||
// Method "can_add_moduleinfo" called in "prepare_new_moduleinfo_data" will handle the capability checks.
|
||||
[, , , , $moduleinfo] = prepare_new_moduleinfo_data($course, $modname, $section->sectionnum);
|
||||
$moduleinfo->beforemod = $targetcmid;
|
||||
create_module((object) $moduleinfo);
|
||||
|
||||
// Adding module affects section structure, and if the module has a delegated section even the course structure.
|
||||
$this->course_state($updates, $course);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ final class create_module_test extends \externallib_advanced_testcase {
|
||||
|
||||
// Execute course action.
|
||||
$results = json_decode(create_module::execute((int)$course->id, $modname, (int)$targetsection->id, (int)$activity->id));
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
// Check result.
|
||||
$cmupdate = $this->find_update_by_fieldname($results, 'put', 'cm', get_string('quickcreatename', 'mod_' . $modname));
|
||||
@@ -122,6 +123,7 @@ final class create_module_test extends \externallib_advanced_testcase {
|
||||
// Execute course action.
|
||||
$modname = 'book';
|
||||
create_module::execute((int)$course->id, $modname, (int)$targetsection->id);
|
||||
$this->assertDebuggingCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace core_courseformat\external;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use moodle_exception;
|
||||
use stdClass;
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
|
||||
|
||||
/**
|
||||
* Tests for the new_module class.
|
||||
*
|
||||
* @package core_courseformat
|
||||
* @category test
|
||||
* @copyright 2024 Mikel Martín <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @coversDefaultClass \core_courseformat\external\new_module
|
||||
*/
|
||||
final class new_module_test extends \externallib_advanced_testcase {
|
||||
|
||||
/**
|
||||
* Setup to ensure that fixtures are loaded.
|
||||
*/
|
||||
public static function setupBeforeClass(): void { // phpcs:ignore
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest.php');
|
||||
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest_output_course_format_state.php');
|
||||
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest_stateactions.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the webservice can execute the new_module action.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_execute(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$modname = 'subsection';
|
||||
$manager = \core_plugin_manager::resolve_plugininfo_class('mod');
|
||||
$manager::enable_plugin($modname, 1);
|
||||
|
||||
// Create a course with an activity.
|
||||
$course = $this->getDataGenerator()->create_course(['numsections' => 1]);
|
||||
$activity = $this->getDataGenerator()->create_module('book', ['course' => $course->id]);
|
||||
$targetsection = get_fast_modinfo($course->id)->get_section_info(1);
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
// Execute course action.
|
||||
$results = json_decode(
|
||||
new_module::execute((int)$course->id, $modname, (int)$targetsection->id, (int)$activity->id),
|
||||
);
|
||||
|
||||
// Check result.
|
||||
$cmupdate = $this->find_update_by_fieldname($results, 'put', 'cm', get_string('quickcreatename', 'mod_' . $modname));
|
||||
$this->assertNotEmpty($cmupdate);
|
||||
$this->assertEquals($modname, $cmupdate->fields->module);
|
||||
$this->assertEquals($targetsection->id, $cmupdate->fields->sectionid);
|
||||
$this->assertEquals(1, $cmupdate->fields->sectionnumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the webservice can execute the new_module action with a format override.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_execute_with_format_override(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$manager = \core_plugin_manager::resolve_plugininfo_class('mod');
|
||||
$manager::enable_plugin('subsection', 1);
|
||||
|
||||
// Create a course.
|
||||
$course = $this->getDataGenerator()->create_course(['format' => 'theunittest', 'numsections' => 1, 'initsections' => 1]);
|
||||
$targetsection = get_fast_modinfo($course->id)->get_section_info(1);
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
// Execute course action.
|
||||
$modname = 'subsection';
|
||||
$results = json_decode(
|
||||
new_module::execute((int)$course->id, $modname, (int)$targetsection->id),
|
||||
);
|
||||
|
||||
// Some course formats doesn't have the renderer file, so a debugging message will be displayed.
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
// Check result.
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the webservice can execute the new_module action with an invalid module.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_execute_with_invalid_module(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create a course.
|
||||
$course = $this->getDataGenerator()->create_course(['numsections' => 1]);
|
||||
$targetsection = get_fast_modinfo($course->id)->get_section_info(1);
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
// Expect exception. Book module doesn't support quickcreate feature.
|
||||
$this->expectException(moodle_exception::class);
|
||||
|
||||
// Execute course action.
|
||||
$modname = 'book';
|
||||
new_module::execute((int)$course->id, $modname, (int)$targetsection->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper methods to find a specific update in the updadelist.
|
||||
*
|
||||
* @param array $updatelist the update list
|
||||
* @param string $action the action to find
|
||||
* @param string $name the element name to find
|
||||
* @param string $fieldname the element identifiername
|
||||
* @return stdClass|null the object found, if any.
|
||||
*/
|
||||
private function find_update_by_fieldname(
|
||||
array $updatelist,
|
||||
string $action,
|
||||
string $name,
|
||||
string $fieldname,
|
||||
|
||||
): ?stdClass {
|
||||
foreach ($updatelist as $update) {
|
||||
if ($update->action != $action || $update->name != $name) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($update->fields->name)) {
|
||||
continue;
|
||||
}
|
||||
if ($update->fields->name == $fieldname) {
|
||||
return $update;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -54,10 +54,28 @@ class stateactions extends core_actions {
|
||||
* @param stateupdates $updates the affected course elements track
|
||||
* @param stdClass $course the course object
|
||||
* @param string $modname the module name
|
||||
* @param int $targetsectionid target section id
|
||||
* @param int $targetsectionnum target section number
|
||||
* @param int|null $targetcmid optional target cm id
|
||||
*/
|
||||
public function create_module(
|
||||
stateupdates $updates,
|
||||
stdClass $course,
|
||||
string $modname,
|
||||
int $targetsectionnum,
|
||||
?int $targetcmid = null
|
||||
): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative new_module state action for testing.
|
||||
*
|
||||
* @param stateupdates $updates the affected course elements track
|
||||
* @param stdClass $course the course object
|
||||
* @param string $modname the module name
|
||||
* @param int $targetsectionid target section id
|
||||
* @param int|null $targetcmid optional target cm id
|
||||
*/
|
||||
public function new_module(
|
||||
stateupdates $updates,
|
||||
stdClass $course,
|
||||
string $modname,
|
||||
|
||||
@@ -1740,8 +1740,6 @@ final class stateactions_test extends \advanced_testcase {
|
||||
|
||||
// Create a course with 1 section and 1 student.
|
||||
$course = $this->getDataGenerator()->create_course(['numsections' => 1]);
|
||||
$student = $this->getDataGenerator()->create_user();
|
||||
$this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
|
||||
$courseformat = course_get_format($course->id);
|
||||
$targetsection = $courseformat->get_modinfo()->get_section_info(1);
|
||||
|
||||
@@ -1762,13 +1760,47 @@ final class stateactions_test extends \advanced_testcase {
|
||||
$this->assertEquals($modname, $cmupdate->module);
|
||||
$this->assertEquals($targetsection->id, $cmupdate->sectionid);
|
||||
$this->assertEquals(get_string('quickcreatename', 'mod_' . $modname), $cmupdate->name);
|
||||
$this->assertDebuggingCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for create_module public method with no capabilities.
|
||||
*
|
||||
* @covers ::create_module
|
||||
*/
|
||||
public function test_create_module_no_capabilities(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$modname = 'subsection';
|
||||
|
||||
// Create a course with 1 section and 1 student.
|
||||
$course = $this->getDataGenerator()->create_course(['numsections' => 1]);
|
||||
$student = $this->getDataGenerator()->create_user();
|
||||
$this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
|
||||
$courseformat = course_get_format($course->id);
|
||||
$targetsection = $courseformat->get_modinfo()->get_section_info(1);
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
// Sanity check.
|
||||
$this->assertEmpty($courseformat->get_modinfo()->get_cms());
|
||||
|
||||
// Change to a user without permission.
|
||||
$this->setUser($student);
|
||||
|
||||
// Validate that the method throws an exception.
|
||||
$this->expectException(moodle_exception::class);
|
||||
$actions->create_module($updates, $course, $modname, $targetsection->sectionnum);
|
||||
$actions = new stateactions();
|
||||
$updates = new stateupdates($courseformat);
|
||||
|
||||
// Capturing exceptions on deprecated methods is tricky because expectException is executed
|
||||
// before assertDebuggingCalled. We need to use try catch in a creative way.
|
||||
try {
|
||||
$actions->create_module($updates, $course, $modname, $targetsection->sectionnum);
|
||||
} catch (moodle_exception $exception) {
|
||||
$this->assertDebuggingCalled();
|
||||
$this->expectException(moodle_exception::class);
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1812,6 +1844,122 @@ final class stateactions_test extends \advanced_testcase {
|
||||
$this->assertEquals($targetsection->id, $cmupdate->sectionid);
|
||||
$this->assertEquals(get_string('quickcreatename', 'mod_' . $modname), $cmupdate->name);
|
||||
|
||||
// Validate that the new module was created between both modules.
|
||||
$this->assertCount(3, $cms);
|
||||
$this->assertArrayHasKey($cmupdate->id, $cms);
|
||||
$this->assertEquals(
|
||||
implode(',', [$forum->cmid, $cmupdate->id, $page->cmid]),
|
||||
$modinfo->get_section_info(1)->sequence
|
||||
);
|
||||
$this->assertDebuggingCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for new_module public method.
|
||||
*
|
||||
* @covers ::new_module
|
||||
*/
|
||||
public function test_new_module(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$modname = 'subsection';
|
||||
|
||||
// Create a course with 1 section and 1 student.
|
||||
$course = $this->getDataGenerator()->create_course(['numsections' => 1]);
|
||||
$courseformat = course_get_format($course->id);
|
||||
$targetsection = $courseformat->get_modinfo()->get_section_info(1);
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
// Sanity check.
|
||||
$this->assertEmpty($courseformat->get_modinfo()->get_cms());
|
||||
|
||||
// Execute given method.
|
||||
$actions = new stateactions();
|
||||
$updates = new stateupdates($courseformat);
|
||||
$actions->new_module($updates, $course, $modname, $targetsection->id);
|
||||
|
||||
// Validate cm was created and updates were generated.
|
||||
$results = $this->summarize_updates($updates);
|
||||
$cmupdate = reset($results['put']['cm']);
|
||||
$this->assertCount(1, $courseformat->get_modinfo()->get_cms());
|
||||
$this->assertEquals($modname, $cmupdate->module);
|
||||
$this->assertEquals($targetsection->id, $cmupdate->sectionid);
|
||||
$this->assertEquals(get_string('quickcreatename', 'mod_' . $modname), $cmupdate->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for new_module public method with no capabilities.
|
||||
*
|
||||
* @covers ::new_module
|
||||
*/
|
||||
public function test_new_module_no_capabilities(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$modname = 'subsection';
|
||||
|
||||
// Create a course with 1 section and 1 student.
|
||||
$course = $this->getDataGenerator()->create_course(['numsections' => 1]);
|
||||
$student = $this->getDataGenerator()->create_user();
|
||||
$this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
|
||||
$courseformat = course_get_format($course->id);
|
||||
$targetsection = $courseformat->get_modinfo()->get_section_info(1);
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
// Sanity check.
|
||||
$this->assertEmpty($courseformat->get_modinfo()->get_cms());
|
||||
|
||||
// Change to a user without permission.
|
||||
$this->setUser($student);
|
||||
|
||||
// Validate that the method throws an exception.
|
||||
$actions = new stateactions();
|
||||
$updates = new stateupdates($courseformat);
|
||||
|
||||
$this->expectException(moodle_exception::class);
|
||||
$actions->new_module($updates, $course, $modname, $targetsection->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for new_module public method with targetcmid parameter.
|
||||
*
|
||||
* @covers ::new_module
|
||||
*/
|
||||
public function test_new_module_with_targetcmid(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$modname = 'subsection';
|
||||
|
||||
// Create a course with 1 section, 2 modules (forum and page) and 1 student.
|
||||
$course = $this->getDataGenerator()->create_course(['numsections' => 1]);
|
||||
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course], ['section' => 1]);
|
||||
$page = $this->getDataGenerator()->create_module('page', ['course' => $course], ['section' => 1]);
|
||||
$student = $this->getDataGenerator()->create_user();
|
||||
$this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
|
||||
$courseformat = course_get_format($course->id);
|
||||
$targetsection = $courseformat->get_modinfo()->get_section_info(1);
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
// Sanity check.
|
||||
$this->assertCount(2, $courseformat->get_modinfo()->get_cms());
|
||||
|
||||
// Execute given method.
|
||||
$actions = new stateactions();
|
||||
$updates = new stateupdates($courseformat);
|
||||
$actions->new_module($updates, $course, $modname, $targetsection->id, $page->cmid);
|
||||
|
||||
$modinfo = $courseformat->get_modinfo();
|
||||
$cms = $modinfo->get_cms();
|
||||
$results = $this->summarize_updates($updates);
|
||||
$cmupdate = reset($results['put']['cm']);
|
||||
|
||||
// Validate updates were generated.
|
||||
$this->assertEquals($modname, $cmupdate->module);
|
||||
$this->assertEquals($targetsection->id, $cmupdate->sectionid);
|
||||
$this->assertEquals(get_string('quickcreatename', 'mod_' . $modname), $cmupdate->name);
|
||||
|
||||
// Validate that the new module was created between both modules.
|
||||
$this->assertCount(3, $cms);
|
||||
$this->assertArrayHasKey($cmupdate->id, $cms);
|
||||
|
||||
+11
-1
@@ -593,6 +593,14 @@ $functions = array(
|
||||
'type' => 'read',
|
||||
'ajax' => true,
|
||||
],
|
||||
'core_courseformat_new_module' => [
|
||||
'classname' => 'core_courseformat\external\new_module',
|
||||
'methodname' => 'execute',
|
||||
'description' => 'Create a new module to course.',
|
||||
'type' => 'write',
|
||||
'ajax' => true,
|
||||
'capabilities' => 'moodle/course:manageactivities',
|
||||
],
|
||||
'core_courseformat_update_course' => [
|
||||
'classname' => 'core_courseformat\external\update_course',
|
||||
'methodname' => 'execute',
|
||||
@@ -604,7 +612,9 @@ $functions = array(
|
||||
'core_courseformat_create_module' => [
|
||||
'classname' => 'core_courseformat\external\create_module',
|
||||
'methodname' => 'execute',
|
||||
'description' => 'Add module to course.',
|
||||
'description' => '** DEPRECATED ** Please do not call this function any more. ' .
|
||||
'Use core_courseformat_new_module instead. ' .
|
||||
'Add module to course.',
|
||||
'type' => 'write',
|
||||
'ajax' => true,
|
||||
'capabilities' => 'moodle/course:manageactivities',
|
||||
|
||||
+2
-1
@@ -46,9 +46,10 @@ class before_activitychooserbutton_exported_handler {
|
||||
|
||||
$attributes = [
|
||||
'class' => 'dropdown-item',
|
||||
'data-action' => 'addModule',
|
||||
'data-action' => 'newModule',
|
||||
'data-modname' => 'subsection',
|
||||
'data-sectionnum' => $section->sectionnum,
|
||||
'data-sectionid' => $section->id,
|
||||
];
|
||||
if ($hook->get_cm()) {
|
||||
$attributes['data-beforemod'] = $hook->get_cm()->id;
|
||||
|
||||
@@ -109,3 +109,15 @@ Feature: Users view subsections on course page
|
||||
Then I should see "New forum" in the "Section 3" "section"
|
||||
And "New database" "text" should appear before "New forum" "text"
|
||||
And "New forum" "text" should appear before "New page" "text"
|
||||
|
||||
@javascript
|
||||
Scenario: Teacher can subsections after moving the parent section
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I open section "1" edit menu
|
||||
And I click on "Move" "link" in the "Section 1" "section"
|
||||
And I click on "Section 3" "link" in the "Move section" "dialogue"
|
||||
And "Section 1" "section" should appear after "Section 3" "section"
|
||||
When I click on "Add content" "button" in the "Section 1" "section"
|
||||
And I click on "Subsection" "link" in the ".dropdown-menu.show" "css_element"
|
||||
Then I should see "New subsection" in the "Section 1" "section"
|
||||
|
||||
Reference in New Issue
Block a user