mirror of
https://github.com/moodle/moodle.git
synced 2025-04-21 08:22:07 +02:00
MDL-81767 courseformat: Add module quickcreation feature
- Add new 'FEATURE_QUICKCREATE' plugin feature that indicates the module can be created with a single click. It will use its own 'quickcreatename' defined string as the initial default name. - Add new 'core_courseformat\external\create_module' web service to create new modules with the quickcreate featuer in the course page. - Add a new 'create_module' state action.
This commit is contained in:
parent
84d9e83588
commit
a0fa76c86b
12
.upgradenotes/MDL-81767-2024082909393804.yml
Normal file
12
.upgradenotes/MDL-81767-2024082909393804.yml
Normal file
@ -0,0 +1,12 @@
|
||||
issueNumber: MDL-81767
|
||||
notes:
|
||||
mod:
|
||||
- message: >-
|
||||
Added new FEATURE_QUICKCREATE for modules that can be quickly created in
|
||||
the course wihout filling a previous form.
|
||||
type: improved
|
||||
core_courseformat:
|
||||
- message: >-
|
||||
Added new 'create_module' webservice to create new module (with
|
||||
quickcreate feature) instances in the course.
|
||||
type: improved
|
2
course/amd/build/actions.min.js
vendored
2
course/amd/build/actions.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -65,7 +65,7 @@ define(
|
||||
const componentActions = [
|
||||
'moveSection', 'moveCm', 'addSection', 'deleteSection', 'cmDelete', 'cmDuplicate', 'sectionHide', 'sectionShow',
|
||||
'cmHide', 'cmShow', 'cmStealth', 'sectionHighlight', 'sectionUnhighlight', 'cmMoveRight', 'cmMoveLeft',
|
||||
'cmNoGroups', 'cmVisibleGroups', 'cmSeparateGroups',
|
||||
'cmNoGroups', 'cmVisibleGroups', 'cmSeparateGroups', 'addModule',
|
||||
];
|
||||
|
||||
// The course reactive instance.
|
||||
|
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
@ -458,6 +458,17 @@ export default class extends BaseComponent {
|
||||
this.reactive.dispatch('addSection', target.dataset.id ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a create subsection request.
|
||||
*
|
||||
* @param {Element} target the dispatch action element
|
||||
* @param {Event} event the triggered event
|
||||
*/
|
||||
async _requestAddModule(target, event) {
|
||||
event.preventDefault();
|
||||
this.reactive.dispatch('addModule', target.dataset.modname, target.dataset.sectionnum, target.dataset.beforemod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a delete section request.
|
||||
*
|
||||
|
@ -66,6 +66,31 @@ export default class {
|
||||
return JSON.parse(ajaxresult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to call core_courseformat_create_module webservice.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a basic section state action.
|
||||
* @param {StateManager} stateManager the current state manager
|
||||
@ -391,6 +416,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} targetSectionNum the target section number
|
||||
* @param {number} targetCmId optional the target cm id
|
||||
*/
|
||||
async addModule(stateManager, modName, targetSectionNum, targetCmId) {
|
||||
if (!modName) {
|
||||
throw new Error(`Mutation addModule requires moduleName`);
|
||||
}
|
||||
if (!targetSectionNum) {
|
||||
throw new Error(`Mutation addModule requires targetSectionNum`);
|
||||
}
|
||||
if (!targetCmId) {
|
||||
targetCmId = 0;
|
||||
}
|
||||
const course = stateManager.get('course');
|
||||
const updates = await this._callAddModuleWebservice(course.id, modName, targetSectionNum, targetCmId);
|
||||
stateManager.processUpdates(updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark or unmark course modules as dragging.
|
||||
*
|
||||
|
144
course/format/classes/external/create_module.php
vendored
Normal file
144
course/format/classes/external/create_module.php
vendored
Normal file
@ -0,0 +1,144 @@
|
||||
<?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 <mikel@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class create_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),
|
||||
'targetsectionnum' => new external_value(PARAM_INT, 'target section number', 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 $targetsectionnum the target section number
|
||||
* @param int|null $targetcmid optional target cm id
|
||||
* @return string Course state in JSON
|
||||
*/
|
||||
public static function execute(
|
||||
int $courseid,
|
||||
string $modname,
|
||||
int $targetsectionnum,
|
||||
?int $targetcmid = null
|
||||
): string {
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/course/lib.php');
|
||||
|
||||
[
|
||||
'courseid' => $courseid,
|
||||
'modname' => $modname,
|
||||
'targetsectionnum' => $targetsectionnum,
|
||||
'targetcmid' => $targetcmid,
|
||||
] = self::validate_parameters(self::execute_parameters(), [
|
||||
'courseid' => $courseid,
|
||||
'modname' => $modname,
|
||||
'targetsectionnum' => $targetsectionnum,
|
||||
'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';
|
||||
}
|
||||
$actions = new $actionsclass();
|
||||
|
||||
$action = 'create_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, $targetsectionnum, $targetcmid);
|
||||
|
||||
// Any state action mark the state cache as dirty.
|
||||
course_format::session_cache_reset($course);
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
@ -1180,4 +1180,35 @@ class stateactions {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a course module.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public function create_module(
|
||||
stateupdates $updates,
|
||||
stdClass $course,
|
||||
string $modname,
|
||||
int $targetsectionnum,
|
||||
?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);
|
||||
|
||||
// Method "can_add_moduleinfo" called in "prepare_new_moduleinfo_data" will handle the capability checks.
|
||||
[, , , , $moduleinfo] = prepare_new_moduleinfo_data($course, $modname, $targetsectionnum);
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
@ -926,5 +926,11 @@ function prepare_new_moduleinfo_data($course, $modulename, $section, string $suf
|
||||
}
|
||||
}
|
||||
|
||||
if (plugin_supports('mod', $data->modulename, FEATURE_QUICKCREATE)) {
|
||||
if (get_string_manager()->string_exists('quickcreatename', "mod_{$data->modulename}")) {
|
||||
$data->name = get_string("quickcreatename", "mod_{$data->modulename}");
|
||||
}
|
||||
}
|
||||
|
||||
return array($module, $context, $cw, $cm, $data);
|
||||
}
|
||||
|
@ -599,6 +599,14 @@ $functions = array(
|
||||
'ajax' => true,
|
||||
'capabilities' => 'moodle/course:sectionvisibility, moodle/course:activityvisibility',
|
||||
],
|
||||
'core_courseformat_create_module' => [
|
||||
'classname' => 'core_courseformat\external\create_module',
|
||||
'methodname' => 'execute',
|
||||
'description' => 'Add module to course.',
|
||||
'type' => 'write',
|
||||
'ajax' => true,
|
||||
'capabilities' => 'moodle/course:manageactivities',
|
||||
],
|
||||
'core_course_edit_module' => array(
|
||||
'classname' => 'core_course_external',
|
||||
'methodname' => 'edit_module',
|
||||
|
@ -500,6 +500,8 @@ define('MOD_PURPOSE_OTHER', 'other');
|
||||
*/
|
||||
define('MOD_PURPOSE_INTERFACE', 'interface');
|
||||
|
||||
/** True if module can be quickly created without filling a previous form. */
|
||||
define('FEATURE_QUICKCREATE', 'quickcreate');
|
||||
/**
|
||||
* Security token used for allowing access
|
||||
* from external application such as web services.
|
||||
|
@ -29,7 +29,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2024082900.00; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
$version = 2024082900.01; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
$release = '4.5dev+ (Build: 20240829)'; // Human-friendly version name
|
||||
|
Loading…
x
Reference in New Issue
Block a user