Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace qbank_managecategories\external;
18
 
19
use core_external\external_api;
20
use core_external\external_function_parameters;
21
use core_external\external_value;
22
use core_external\external_single_structure;
23
use core_external\external_multiple_structure;
24
use core_question\category_manager;
25
use moodle_exception;
26
use context;
27
use qbank_managecategories\helper;
28
 
29
/**
30
 * External class used for category reordering.
31
 *
32
 * @package    qbank_managecategories
33
 * @category   external
34
 * @copyright  2024 Catalyst IT Europe Ltd.
35
 * @author     Mark Johnson <mark.johnson@catalyst-eu.net>
36
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37
 */
38
class move_category extends external_api {
39
    /**
40
     * Describes the parameters for update_category_order webservice.
41
     * @return external_function_parameters
42
     */
43
    public static function execute_parameters(): external_function_parameters {
44
        return new external_function_parameters([
45
            'pagecontextid' => new external_value(PARAM_INT, 'The context of the current page'),
46
            'categoryid' => new external_value(PARAM_INT, 'Category being moved'),
47
            'targetparentid' => new external_value(PARAM_INT, 'The ID of the parent category to move to.'),
48
            'precedingsiblingid' => new external_value(
49
                PARAM_INT,
50
                'The ID of the preceding category. Null if this is being moved to top of its parent',
51
                allownull: NULL_ALLOWED,
52
            ),
53
        ]);
54
    }
55
 
56
    /**
57
     * Returns description of method result value.
58
     *
59
     * This function will always return a set of state updates for the core/reactive state.
60
     * {@link https://moodledev.io/docs/4.2/guides/javascript/reactive#controlling-the-state-from-the-backend}
61
     *
62
     * @return external_multiple_structure
63
     */
64
    public static function execute_returns(): external_multiple_structure {
65
        return new external_multiple_structure(
66
            new external_single_structure(
67
                [
68
                    'name' => new external_value(PARAM_ALPHA, 'State object name (always "categories" from this function).'),
69
                    'action' => new external_value(PARAM_ALPHA, 'State update type (always "put" from this function).'),
70
                    'fields' => new external_single_structure(
71
                        [
72
                            'id' => new external_value(PARAM_INT, 'The ID of the category that was updated.'),
73
                            'sortorder' => new external_value(PARAM_INT, 'The new sortorder', VALUE_OPTIONAL),
74
                            'parent' => new external_value(PARAM_INT, 'The ID of the new parent category.', VALUE_OPTIONAL),
75
                            'context' => new external_value(PARAM_INT, 'The ID of the new context.', VALUE_OPTIONAL),
76
                            'draghandle' => new external_value(
77
                                PARAM_BOOL,
78
                                'Should this category have a drag handle?',
79
                                VALUE_OPTIONAL
80
                            ),
81
                        ]
82
                    ),
83
                ],
84
                'An individual state update',
85
            ),
86
            'Category state updates',
87
        );
88
    }
89
 
90
    /**
91
     * Move category to new location.
92
     *
93
     * @param int $pagecontextid ID of the context of the current page.
94
     * @param int $categoryid ID of the category to move.
95
     * @param int $targetparentid The ID of the parent category to move to.
96
     * @param ?int $precedingsiblingid The ID of the preceding category. Null if this is being moved to top of its parent.
97
     * @return array Reactive state updates representing the changes made to the categories.
98
     */
99
    public static function execute(
100
        int $pagecontextid,
101
        int $categoryid,
102
        int $targetparentid,
103
        ?int $precedingsiblingid = null
104
    ): array {
105
        // Update category location.
106
        global $DB, $CFG;
107
 
108
        require_once($CFG->libdir . '/questionlib.php');
109
 
110
        $context = context::instance_by_id($pagecontextid);
111
        self::validate_context($context);
112
        $manager = new category_manager();
113
 
114
        $origincategory = $DB->get_record('question_categories', ['id' => $categoryid], '*', MUST_EXIST);
115
        $targetparent = $DB->get_record('question_categories', ['id' => $targetparentid], '*', MUST_EXIST);
116
        if ($precedingsiblingid) {
117
            $precedingsibling = $DB->get_record('question_categories', ['id' => $precedingsiblingid], '*', MUST_EXIST);
118
        }
119
 
120
        // Check permission for original and destination contexts.
121
        $manager->require_manage_category(context::instance_by_id($origincategory->contextid));
122
 
123
        if ($origincategory->contextid != $targetparent->contextid) {
124
            $manager->require_manage_category(context::instance_by_id($targetparent->contextid));
125
        }
126
 
127
        $originstateupdate = self::make_state_update($origincategory->id);
128
        $stateupdates = [];
129
 
130
        $transaction = $DB->start_delegated_transaction();
131
 
132
        // Set new parent.
133
        if ($origincategory->parent !== $targetparent->id) {
134
            $newsiblings = $DB->get_fieldset('question_categories', 'id', ['parent' => $targetparent->id]);
135
            if (
136
                count($newsiblings) == 1
137
                && $manager->is_only_child_of_top_category_in_context(reset($newsiblings))
138
            ) {
139
                // If we are moving to a top-level parent that only had 1 category before, allow re-ordering of that category.
140
                $stateupdates[] = self::make_state_update(reset($newsiblings), draghandle: true);
141
            }
142
            $originstateupdate->fields->parent = $targetparent->id;
143
        }
144
 
145
        // Change to the same context.
146
        if ($origincategory->contextid !== $targetparent->contextid) {
147
            // Check for duplicate idnumber.
148
            if (
149
                !is_null($origincategory->idnumber)
150
                && !$manager->idnumber_is_unique_in_context($origincategory->idnumber, $targetparent->contextid)
151
            ) {
152
                $transaction->rollback(new moodle_exception('idnumberexists', 'qbank_managecategories'));
153
            }
154
            $originstateupdate->fields->context = $targetparent->contextid;
155
        }
156
 
157
        // Update sort order.
158
        if ($precedingsiblingid) {
159
            $sortorder = $precedingsibling->sortorder + 1;
160
        } else {
161
            $sortorder = 1;
162
        }
163
        $originstateupdate->fields->sortorder = $sortorder;
164
 
165
        // Save the updated parent, context and sortorder.
166
        $manager->update_category(
167
            $categoryid,
168
            helper::combine_id_context($targetparent),
169
            $origincategory->name,
170
            $origincategory->info,
171
            $origincategory->infoformat,
172
            $origincategory->idnumber,
173
            $sortorder,
174
        );
175
 
176
        // Get other categories which are after the new position, and update their sortorder.
177
        $params = [
178
            'parent' => $targetparent->id,
179
            'sortorder' => $sortorder,
180
            'origincategoryid' => $origincategory->id,
181
        ];
182
        $select = "
183
            parent = :parent
184
            AND id <> :origincategoryid
185
            AND sortorder >= :sortorder";
186
        $sort = "sortorder ASC";
187
        $toupdatesortorder = $DB->get_records_select('question_categories', $select, $params, $sort);
188
        foreach ($toupdatesortorder as $category) {
189
            $DB->set_field('question_categories', 'sortorder', ++$sortorder, ['id' => $category->id]);
190
            $stateupdates[] = self::make_state_update($category->id, sortorder: $sortorder);
191
        }
192
 
193
        if (isset($originstateupdate->fields->parent)) {
194
            // If the category has moved parent, re-order the original siblings to fill the gap.
195
            $originsortorder = 1;
196
            $params = [
197
                'parent' => $origincategory->parent,
198
            ];
199
            $select = "parent = :parent";
200
            $sort = "sortorder ASC";
201
            $originsiblings = $DB->get_records_select('question_categories', $select, $params, $sort);
202
            if (
203
                count($originsiblings) == 1
204
                && $manager->is_only_child_of_top_category_in_context(reset($originsiblings)->id)
205
            ) {
206
                // If this is now the only category in the context, don't allow re-ordering.
207
                $stateupdates[] = self::make_state_update(
208
                    reset($originsiblings)->id,
209
                    sortorder: $originsortorder,
210
                    draghandle: false,
211
                );
212
            } else {
213
                foreach ($originsiblings as $category) {
214
                    if ($category->sortorder !== $originsortorder) {
215
                        $DB->set_field('question_categories', 'sortorder', $originsortorder, ['id' => $category->id]);
216
                        $stateupdates[] = self::make_state_update($category->id, sortorder: $originsortorder);
217
                    }
218
                    $originsortorder++;
219
                }
220
            }
221
        }
222
 
223
        $transaction->allow_commit();
224
 
225
        // Return the updated for the moved category, followed by any additional updates that happened as a result.
226
        array_unshift($stateupdates, $originstateupdate);
227
 
228
        return $stateupdates;
229
    }
230
 
231
    /**
232
     * Generate a category state update based on the provided fields.
233
     *
234
     * @param int $id Category ID, required.
235
     * @param int|null $sortorder New sortorder, optional.
236
     * @param int|null $parent Category ID of new parent, optional.
237
     * @param bool|null $draghandle Set display of the drag handle. Optional.
238
     * @return \stdClass The update object.
239
     */
240
    protected static function make_state_update(
241
        int $id,
242
        ?int $sortorder = null,
243
        ?int $parent = null,
244
        ?bool $draghandle = null,
245
    ): \stdClass {
246
        $update = (object)[
247
            'name' => 'categories',
248
            'action' => 'put',
249
            'fields' => (object)[
250
                'id' => $id,
251
            ],
252
        ];
253
        if (!is_null($sortorder)) {
254
            $update->fields->sortorder = $sortorder;
255
        }
256
        if (!is_null($parent)) {
257
            $update->fields->parent = $parent;
258
        }
259
        if (!is_null($draghandle)) {
260
            $update->fields->draghandle = $draghandle;
261
        }
262
        return $update;
263
    }
264
}