AutorÃa | Ultima modificación | Ver Log |
<?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 qbank_managecategories\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_value;
use core_external\external_single_structure;
use core_external\external_multiple_structure;
use core_question\category_manager;
use moodle_exception;
use context;
use qbank_managecategories\helper;
/**
* External class used for category reordering.
*
* @package qbank_managecategories
* @category external
* @copyright 2024 Catalyst IT Europe Ltd.
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class move_category extends external_api {
/**
* Describes the parameters for update_category_order webservice.
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'pagecontextid' => new external_value(PARAM_INT, 'The context of the current page'),
'categoryid' => new external_value(PARAM_INT, 'Category being moved'),
'targetparentid' => new external_value(PARAM_INT, 'The ID of the parent category to move to.'),
'precedingsiblingid' => new external_value(
PARAM_INT,
'The ID of the preceding category. Null if this is being moved to top of its parent',
allownull: NULL_ALLOWED,
),
]);
}
/**
* Returns description of method result value.
*
* This function will always return a set of state updates for the core/reactive state.
* {@link https://moodledev.io/docs/4.2/guides/javascript/reactive#controlling-the-state-from-the-backend}
*
* @return external_multiple_structure
*/
public static function execute_returns(): external_multiple_structure {
return new external_multiple_structure(
new external_single_structure(
[
'name' => new external_value(PARAM_ALPHA, 'State object name (always "categories" from this function).'),
'action' => new external_value(PARAM_ALPHA, 'State update type (always "put" from this function).'),
'fields' => new external_single_structure(
[
'id' => new external_value(PARAM_INT, 'The ID of the category that was updated.'),
'sortorder' => new external_value(PARAM_INT, 'The new sortorder', VALUE_OPTIONAL),
'parent' => new external_value(PARAM_INT, 'The ID of the new parent category.', VALUE_OPTIONAL),
'context' => new external_value(PARAM_INT, 'The ID of the new context.', VALUE_OPTIONAL),
'draghandle' => new external_value(
PARAM_BOOL,
'Should this category have a drag handle?',
VALUE_OPTIONAL
),
]
),
],
'An individual state update',
),
'Category state updates',
);
}
/**
* Move category to new location.
*
* @param int $pagecontextid ID of the context of the current page.
* @param int $categoryid ID of the category to move.
* @param int $targetparentid The ID of the parent category to move to.
* @param ?int $precedingsiblingid The ID of the preceding category. Null if this is being moved to top of its parent.
* @return array Reactive state updates representing the changes made to the categories.
*/
public static function execute(
int $pagecontextid,
int $categoryid,
int $targetparentid,
?int $precedingsiblingid = null
): array {
// Update category location.
global $DB, $CFG;
require_once($CFG->libdir . '/questionlib.php');
$context = context::instance_by_id($pagecontextid);
self::validate_context($context);
$manager = new category_manager();
$origincategory = $DB->get_record('question_categories', ['id' => $categoryid], '*', MUST_EXIST);
$targetparent = $DB->get_record('question_categories', ['id' => $targetparentid], '*', MUST_EXIST);
if ($precedingsiblingid) {
$precedingsibling = $DB->get_record('question_categories', ['id' => $precedingsiblingid], '*', MUST_EXIST);
}
// Check permission for original and destination contexts.
$manager->require_manage_category(context::instance_by_id($origincategory->contextid));
if ($origincategory->contextid != $targetparent->contextid) {
$manager->require_manage_category(context::instance_by_id($targetparent->contextid));
}
$originstateupdate = self::make_state_update($origincategory->id);
$stateupdates = [];
$transaction = $DB->start_delegated_transaction();
// Set new parent.
if ($origincategory->parent !== $targetparent->id) {
$newsiblings = $DB->get_fieldset('question_categories', 'id', ['parent' => $targetparent->id]);
if (
count($newsiblings) == 1
&& $manager->is_only_child_of_top_category_in_context(reset($newsiblings))
) {
// If we are moving to a top-level parent that only had 1 category before, allow re-ordering of that category.
$stateupdates[] = self::make_state_update(reset($newsiblings), draghandle: true);
}
$originstateupdate->fields->parent = $targetparent->id;
}
// Change to the same context.
if ($origincategory->contextid !== $targetparent->contextid) {
// Check for duplicate idnumber.
if (
!is_null($origincategory->idnumber)
&& !$manager->idnumber_is_unique_in_context($origincategory->idnumber, $targetparent->contextid)
) {
$transaction->rollback(new moodle_exception('idnumberexists', 'qbank_managecategories'));
}
$originstateupdate->fields->context = $targetparent->contextid;
}
// Update sort order.
if ($precedingsiblingid) {
$sortorder = $precedingsibling->sortorder + 1;
} else {
$sortorder = 1;
}
$originstateupdate->fields->sortorder = $sortorder;
// Save the updated parent, context and sortorder.
$manager->update_category(
$categoryid,
helper::combine_id_context($targetparent),
$origincategory->name,
$origincategory->info,
$origincategory->infoformat,
$origincategory->idnumber,
$sortorder,
);
// Get other categories which are after the new position, and update their sortorder.
$params = [
'parent' => $targetparent->id,
'sortorder' => $sortorder,
'origincategoryid' => $origincategory->id,
];
$select = "
parent = :parent
AND id <> :origincategoryid
AND sortorder >= :sortorder";
$sort = "sortorder ASC";
$toupdatesortorder = $DB->get_records_select('question_categories', $select, $params, $sort);
foreach ($toupdatesortorder as $category) {
$DB->set_field('question_categories', 'sortorder', ++$sortorder, ['id' => $category->id]);
$stateupdates[] = self::make_state_update($category->id, sortorder: $sortorder);
}
if (isset($originstateupdate->fields->parent)) {
// If the category has moved parent, re-order the original siblings to fill the gap.
$originsortorder = 1;
$params = [
'parent' => $origincategory->parent,
];
$select = "parent = :parent";
$sort = "sortorder ASC";
$originsiblings = $DB->get_records_select('question_categories', $select, $params, $sort);
if (
count($originsiblings) == 1
&& $manager->is_only_child_of_top_category_in_context(reset($originsiblings)->id)
) {
// If this is now the only category in the context, don't allow re-ordering.
$stateupdates[] = self::make_state_update(
reset($originsiblings)->id,
sortorder: $originsortorder,
draghandle: false,
);
} else {
foreach ($originsiblings as $category) {
if ($category->sortorder !== $originsortorder) {
$DB->set_field('question_categories', 'sortorder', $originsortorder, ['id' => $category->id]);
$stateupdates[] = self::make_state_update($category->id, sortorder: $originsortorder);
}
$originsortorder++;
}
}
}
$transaction->allow_commit();
// Return the updated for the moved category, followed by any additional updates that happened as a result.
array_unshift($stateupdates, $originstateupdate);
return $stateupdates;
}
/**
* Generate a category state update based on the provided fields.
*
* @param int $id Category ID, required.
* @param int|null $sortorder New sortorder, optional.
* @param int|null $parent Category ID of new parent, optional.
* @param bool|null $draghandle Set display of the drag handle. Optional.
* @return \stdClass The update object.
*/
protected static function make_state_update(
int $id,
?int $sortorder = null,
?int $parent = null,
?bool $draghandle = null,
): \stdClass {
$update = (object)[
'name' => 'categories',
'action' => 'put',
'fields' => (object)[
'id' => $id,
],
];
if (!is_null($sortorder)) {
$update->fields->sortorder = $sortorder;
}
if (!is_null($parent)) {
$update->fields->parent = $parent;
}
if (!is_null($draghandle)) {
$update->fields->draghandle = $draghandle;
}
return $update;
}
}