MDL-42629 course: management resorting improvements
[moodle.git] / course / management.php
CommitLineData
5dc361e1
SH
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/**
18 * Course and category management interfaces.
19 *
20 * @package core_course
21 * @copyright 2013 Sam Hemelryk
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25require_once('../config.php');
26require_once($CFG->dirroot.'/lib/coursecatlib.php');
27require_once($CFG->dirroot.'/course/lib.php');
28
29$categoryid = optional_param('categoryid', null, PARAM_INT);
cda49969 30$selectedcategoryid = optional_param('selectedcategoryid', null, PARAM_INT);
5dc361e1
SH
31$courseid = optional_param('courseid', null, PARAM_INT);
32$action = optional_param('action', false, PARAM_ALPHA);
33$page = optional_param('page', 0, PARAM_INT);
34$perpage = optional_param('perpage', null, PARAM_INT);
35$viewmode = optional_param('view', 'default', PARAM_ALPHA); // Can be one of default, combined, courses, or categories.
36
37// Search related params.
38$search = optional_param('search', '', PARAM_RAW); // Search words. Shortname, fullname, idnumber and summary get searched.
39$blocklist = optional_param('blocklist', 0, PARAM_INT); // Find courses containing this block.
40$modulelist = optional_param('modulelist', '', PARAM_PLUGIN); // Find courses containing the given modules.
41
484c4c6c
SH
42if (!in_array($viewmode, array('default', 'combined', 'courses', 'categories'))) {
43 $viewmode = 'default';
44}
45
5dc361e1
SH
46$issearching = ($search !== '' || $blocklist !== 0 || $modulelist !== '');
47if ($issearching) {
48 $viewmode = 'courses';
49}
50
51$url = new moodle_url('/course/management.php');
b488058f 52$systemcontext = $context = context_system::instance();
5dc361e1
SH
53if ($courseid) {
54 $record = get_course($courseid);
55 $course = new course_in_list($record);
56 $category = coursecat::get($course->category);
57 $categoryid = $category->id;
b488058f 58 $context = context_coursecat::instance($category->id);
d5814f4e
PS
59 $url->param('categoryid', $categoryid);
60 navigation_node::override_active_url($url);
5dc361e1 61 $url->param('courseid', $course->id);
d5814f4e 62
5dc361e1
SH
63} else if ($categoryid) {
64 $courseid = null;
65 $course = null;
66 $category = coursecat::get($categoryid);
b488058f 67 $context = context_coursecat::instance($category->id);
5dc361e1 68 $url->param('categoryid', $category->id);
d5814f4e
PS
69 navigation_node::override_active_url($url);
70
5dc361e1
SH
71} else {
72 $course = null;
73 $courseid = null;
74 $category = null;
75 $categoryid = null;
76 if ($viewmode === 'default') {
77 $viewmode = 'categories';
78 }
b488058f 79 $context = $systemcontext;
d5814f4e 80 navigation_node::override_active_url($url);
5dc361e1 81}
484c4c6c 82
cda49969
SH
83// Check if there is a selected category param, and if there is apply it.
84if ($course === null && $selectedcategoryid !== null && $selectedcategoryid !== $categoryid) {
85 $url->param('categoryid', $selectedcategoryid);
86}
87
5dc361e1
SH
88if ($page !== 0) {
89 $url->param('page', $page);
90}
91if ($viewmode !== 'default') {
92 $url->param('view', $viewmode);
93}
94if ($search !== '') {
95 $url->param('search', $search);
96}
97if ($blocklist !== 0) {
98 $url->param('blocklist', $search);
99}
100if ($modulelist !== '') {
101 $url->param('modulelist', $search);
102}
103
62846237 104$strmanagement = new lang_string('coursecatmanagement');
b488058f 105$title = format_string($SITE->fullname, true, array('context' => $systemcontext));
5dc361e1
SH
106
107$PAGE->set_context($context);
108$PAGE->set_url($url);
109$PAGE->set_pagelayout('admin');
110$PAGE->set_title($title);
62846237 111$PAGE->set_heading($strmanagement);
5dc361e1 112
962885e8
SH
113// This is a system level page that operates on other contexts.
114require_login();
115
484c4c6c
SH
116if (!coursecat::has_capability_on_any(array('moodle/category:manage', 'moodle/course:create'))) {
117 // The user isn't able to manage any categories. Lets redirect them to the relevant course/index.php page.
118 $url = new moodle_url('/course/index.php');
119 if ($categoryid) {
120 $url->param('categoryid', $categoryid);
121 }
122 redirect($url);
123}
124
b488058f
SH
125// If the user poses any of these capabilities then they will be able to see the admin
126// tree and the management link within it.
127// This is the most accurate form of navigation.
128$capabilities = array(
129 'moodle/site:config',
130 'moodle/backup:backupcourse',
131 'moodle/category:manage',
132 'moodle/course:create',
133 'moodle/site:approvecourse'
134);
135if ($category && !has_any_capability($capabilities, $systemcontext)) {
484c4c6c
SH
136 // If the user doesn't poses any of these system capabilities then we're going to mark the manage link in the settings block
137 // as active, tell the page to ignore the active path and just build what the user would expect.
b488058f 138 // This will at least give the page some relevant navigation.
b488058f 139 navigation_node::override_active_url(new moodle_url('/course/management.php', array('categoryid' => $category->id)));
484c4c6c
SH
140 $PAGE->set_category_by_id($category->id);
141 $PAGE->navbar->ignore_active(true);
142 $PAGE->navbar->add(get_string('coursemgmt', 'admin'), $PAGE->url->out_omit_querystring());
143}
144if (!$issearching && $category !== null) {
5dc361e1 145 $parents = coursecat::get_many($category->get_parents());
484c4c6c 146 $parents[] = $category;
5dc361e1 147 foreach ($parents as $parent) {
484c4c6c
SH
148 $PAGE->navbar->add(
149 $parent->get_formatted_name(),
150 new moodle_url('/course/management.php', array('categoryid' => $parent->id))
151 );
5dc361e1 152 }
5dc361e1
SH
153 if ($course instanceof course_in_list) {
154 // Use the list name so that it matches whats being displayed below.
155 $PAGE->navbar->add($course->get_formatted_name());
156 }
157}
158
5dc361e1
SH
159$notificationspass = array();
160$notificationsfail = array();
161
162if ($action !== false && confirm_sesskey()) {
163 // Actions:
164 // - resortcategories : Resort the courses in the given category.
165 // - resortcourses : Resort courses
166 // - showcourse : make a course visible.
167 // - hidecourse : make a course hidden.
168 // - movecourseup : move the selected course up one.
169 // - movecoursedown : move the selected course down.
170 // - showcategory : make a category visible.
171 // - hidecategory : make a category hidden.
172 // - movecategoryup : move category up.
173 // - movecategorydown : move category down.
174 // - deletecategory : delete the category either in full, or moving contents.
175 // - bulkaction : performs bulk actions:
176 // - bulkmovecourses.
177 // - bulkmovecategories.
178 // - bulkresortcategories.
179 $redirectback = false;
180 switch ($action) {
181 case 'resortcategories' :
182 $sort = required_param('resort', PARAM_ALPHA);
b488058f
SH
183 $cattosort = coursecat::get((int)optional_param('categoryid', 0, PARAM_INT));
184 $redirectback = \core_course\management\helper::action_category_resort_subcategories($cattosort, $sort);
5dc361e1
SH
185 break;
186 case 'resortcourses' :
187 // They must have specified a category.
188 required_param('categoryid', PARAM_INT);
189 $sort = required_param('resort', PARAM_ALPHA);
190 \core_course\management\helper::action_category_resort_courses($category, $sort);
191 break;
192 case 'showcourse' :
193 $redirectback = \core_course\management\helper::action_course_show($course);
194 break;
195 case 'hidecourse' :
196 $redirectback = \core_course\management\helper::action_course_hide($course);
197 break;
198 case 'movecourseup' :
199 // They must have specified a category and a course.
200 required_param('categoryid', PARAM_INT);
201 required_param('courseid', PARAM_INT);
5aff38e4 202 $redirectback = \core_course\management\helper::action_course_change_sortorder_up_one($course, $category);
5dc361e1
SH
203 break;
204 case 'movecoursedown' :
205 // They must have specified a category and a course.
206 required_param('categoryid', PARAM_INT);
207 required_param('courseid', PARAM_INT);
5aff38e4 208 $redirectback = \core_course\management\helper::action_course_change_sortorder_down_one($course, $category);
5dc361e1
SH
209 break;
210 case 'showcategory' :
211 // They must have specified a category.
212 required_param('categoryid', PARAM_INT);
213 $redirectback = \core_course\management\helper::action_category_show($category);
214 break;
215 case 'hidecategory' :
216 // They must have specified a category.
217 required_param('categoryid', PARAM_INT);
218 $redirectback = \core_course\management\helper::action_category_hide($category);
219 break;
220 case 'movecategoryup' :
221 // They must have specified a category.
222 required_param('categoryid', PARAM_INT);
5aff38e4 223 $redirectback = \core_course\management\helper::action_category_change_sortorder_up_one($category);
5dc361e1
SH
224 break;
225 case 'movecategorydown' :
226 // They must have specified a category.
227 required_param('categoryid', PARAM_INT);
5aff38e4 228 $redirectback = \core_course\management\helper::action_category_change_sortorder_down_one($category);
5dc361e1
SH
229 break;
230 case 'deletecategory':
231 // They must have specified a category.
232 required_param('categoryid', PARAM_INT);
233 if (!$category->can_delete()) {
234 throw new moodle_exception('permissiondenied', 'error', '', null, 'coursecat::can_resort');
235 }
236 require_once($CFG->dirroot.'/course/delete_category_form.php');
67e1f268 237 $mform = new core_course_deletecategory_form(null, $category);
5dc361e1 238 if ($mform->is_cancelled()) {
484c4c6c 239 redirect($PAGE->url);
5dc361e1
SH
240 }
241 // Start output.
242 /* @var core_course_management_renderer|core_renderer $renderer */
243 $renderer = $PAGE->get_renderer('core_course', 'management');
244 echo $renderer->header();
245 echo $renderer->heading(get_string('deletecategory', 'moodle', $category->get_formatted_name()));
246
247 if ($data = $mform->get_data()) {
248 // The form has been submit handle it.
249 if ($data->fulldelete == 1 && $category->can_delete_full()) {
8aa3aa3d
SH
250 $continueurl = new moodle_url('/course/management.php');
251 if ($category->parent != '0') {
252 $continueurl->param('categoryid', $category->parent);
253 }
5dc361e1
SH
254 $notification = get_string('coursecategorydeleted', '', $category->get_formatted_name());
255 $deletedcourses = $category->delete_full(true);
256 foreach ($deletedcourses as $course) {
257 echo $renderer->notification(get_string('coursedeleted', '', $course->shortname), 'notifysuccess');
258 }
259 echo $renderer->notification($notification, 'notifysuccess');
260 echo $renderer->continue_button($continueurl);
261 } else if ($data->fulldelete == 0 && $category->can_move_content_to($data->newparent)) {
262 $continueurl = new moodle_url('/course/management.php', array('categoryid' => $data->newparent));
263 $category->delete_move($data->newparent, true);
264 echo $renderer->continue_button($continueurl);
265 } else {
266 // Some error in parameters (user is cheating?)
267 $mform->display();
268 }
269 } else {
270 // Display the form.
271 $mform->display();
272 }
273 // Finish output and exit.
274 echo $renderer->footer();
275 exit();
276 break;
277 case 'bulkaction':
278 $bulkmovecourses = optional_param('bulkmovecourses', false, PARAM_BOOL);
279 $bulkmovecategories = optional_param('bulkmovecategories', false, PARAM_BOOL);
c7a2291f 280 $bulkresortcategories = optional_param('bulksort', false, PARAM_BOOL);
5dc361e1
SH
281
282 if ($bulkmovecourses) {
283 // Move courses out of the current category and into a new category.
284 // They must have specified a category.
285 required_param('categoryid', PARAM_INT);
286 $movetoid = required_param('movecoursesto', PARAM_INT);
287 $courseids = optional_param_array('bc', false, PARAM_INT);
288 if ($courseids === false) {
289 break;
290 }
291 $moveto = coursecat::get($movetoid);
67e1f268
SH
292 try {
293 // If this fails we want to catch the exception and report it.
294 $redirectback = \core_course\management\helper::action_category_move_courses_into($category, $moveto,
295 $courseids);
296 } catch (moodle_exception $ex) {
297 $redirectback = false;
298 $notificationsfail[] = $ex->getMessage();
299 }
5dc361e1 300 } else if ($bulkmovecategories) {
d61c05ac 301 $categoryids = optional_param_array('bcat', array(), PARAM_INT);
5dc361e1
SH
302 $movetocatid = required_param('movecategoriesto', PARAM_INT);
303 $movetocat = coursecat::get($movetocatid);
304 $movecount = 0;
305 foreach ($categoryids as $id) {
306 $cattomove = coursecat::get($id);
307 if ($id == $movetocatid) {
308 $notificationsfail[] = get_string('movecategoryownparent', 'error', $cattomove->get_formatted_name());
309 continue;
310 }
311 if (strpos($movetocat->path, $cattomove->path) === 0) {
312 $notificationsfail[] = get_string('movecategoryparentconflict', 'error', $cattomove->get_formatted_name());
313 continue;
314 }
315 if ($cattomove->parent != $movetocatid) {
316 if ($cattomove->can_change_parent($movetocatid)) {
317 $cattomove->change_parent($movetocatid);
318 $movecount++;
319 } else {
320 $notificationsfail[] = get_string('movecategorynotpossible', 'error', $cattomove->get_formatted_name());
321 }
322 }
323 }
324 if ($movecount > 1) {
325 $a = new stdClass;
326 $a->count = $movecount;
327 $a->to = $movetocat->get_formatted_name();
3b732cd6
RT
328 $movesuccessstrkey = 'movecategoriessuccess';
329 if ($movetocatid == 0) {
330 $movesuccessstrkey = 'movecategoriestotopsuccess';
331 }
332 $notificationspass[] = get_string($movesuccessstrkey, 'moodle', $a);
5dc361e1
SH
333 } else if ($movecount === 1) {
334 $a = new stdClass;
335 $a->moved = $cattomove->get_formatted_name();
336 $a->to = $movetocat->get_formatted_name();
3b732cd6
RT
337 $movesuccessstrkey = 'movecategorysuccess';
338 if ($movetocatid == 0) {
339 $movesuccessstrkey = 'movecategorytotopsuccess';
340 }
341 $notificationspass[] = get_string($movesuccessstrkey, 'moodle', $a);
5dc361e1
SH
342 }
343 } else if ($bulkresortcategories) {
c7a2291f
SH
344 $for = required_param('selectsortby', PARAM_ALPHA);
345 $sortcategoriesby = required_param('resortcategoriesby', PARAM_ALPHA);
346 $sortcoursesby = required_param('resortcoursesby', PARAM_ALPHA);
347
348 if ($sortcategoriesby === 'none' && $sortcoursesby === 'none') {
349 // They're not sorting anything.
350 break;
351 }
352
353 if ($for === 'thiscategory') {
354 $categoryids = array(
355 required_param('currentcategoryid', PARAM_INT)
356 );
357 $categories = coursecat::get_many($categoryids);
358 } else if ($for === 'selectedcategories') {
359 // Bulk resort selected categories.
360 $categoryids = optional_param_array('bcat', false, PARAM_INT);
361 $sort = required_param('resortcategoriesby', PARAM_ALPHA);
362 if ($categoryids === false) {
363 break;
364 }
365 $categories = coursecat::get_many($categoryids);
366 } else if ($for === 'allcategories') {
367 $categories = coursecat::get_all_visible();
368 } else {
5dc361e1
SH
369 break;
370 }
c7a2291f
SH
371 if (!in_array($sortcategoriesby, array('idnumber', 'name'))) {
372 $sortcategoriesby = false;
373 }
374 if (!in_array($sortcoursesby, array('idnumber', 'fullname', 'shortname'))) {
375 $sortcoursesby = false;
376 }
5dc361e1 377 foreach ($categories as $cat) {
c7a2291f
SH
378 if ($sortcategoriesby) {
379 // Don't clean up here, we'll do it once we're all done.
380 \core_course\management\helper::action_category_resort_subcategories($cat, $sortcategoriesby, false);
381 }
382 if (in_array($sortcoursesby, array('idnumber', 'fullname', 'shortname'))) {
383 \core_course\management\helper::action_category_resort_courses($cat, $sortcoursesby, false);
384 }
5dc361e1 385 }
c7a2291f 386 coursecat::resort_categories_cleanup($sortcoursesby !== false);
b488058f
SH
387 if ($category === null && count($categoryids) === 1) {
388 // They're bulk sorting just a single category and they've not selected a category.
389 // Lets for convenience sake auto-select the category that has been resorted for them.
390 redirect(new moodle_url($PAGE->url, array('categoryid' => reset($categoryids))));
391 }
5dc361e1
SH
392 }
393 }
394 if ($redirectback) {
395 redirect($PAGE->url);
396 }
397}
398
399if (!is_null($perpage)) {
400 set_user_preference('coursecat_management_perpage', $perpage);
401} else {
402 $perpage = get_user_preferences('coursecat_management_perpage', $CFG->coursesperpage);
403}
404if ((int)$perpage != $perpage || $perpage < 2) {
405 $perpage = $CFG->coursesperpage;
406}
407
408$categorysize = 4;
409$coursesize = 4;
410$detailssize = 4;
411if ($viewmode === 'default' || $viewmode === 'combined') {
412 if (isset($courseid)) {
413 $class = 'columns-3';
414 } else {
415 $categorysize = 5;
416 $coursesize = 7;
417 $class = 'columns-2';
418 }
419} else if ($viewmode === 'categories') {
420 $categorysize = 12;
421 $class = 'columns-1';
422} else if ($viewmode === 'courses') {
423 if (isset($courseid)) {
424 $coursesize = 6;
425 $detailssize = 6;
426 $class = 'columns-2';
427 } else {
428 $coursesize = 12;
429 $class = 'columns-1';
430 }
431}
484c4c6c
SH
432if ($viewmode === 'default' || $viewmode === 'combined') {
433 $class .= ' viewmode-cobmined';
434} else {
435 $class .= ' viewmode-'.$viewmode;
436}
437if (($viewmode === 'default' || $viewmode === 'combined' || $viewmode === 'courses') && !empty($courseid)) {
438 $class .= ' course-selected';
439}
5dc361e1
SH
440
441/* @var core_course_management_renderer|core_renderer $renderer */
442$renderer = $PAGE->get_renderer('core_course', 'management');
443$renderer->enhance_management_interface();
444
445echo $renderer->header();
446
447if (!$issearching) {
62846237 448 echo $renderer->management_heading($strmanagement, $viewmode, $categoryid);
5dc361e1
SH
449} else {
450 echo $renderer->management_heading(new lang_string('searchresults'));
451}
452
453if (count($notificationspass) > 0) {
454 echo $renderer->notification(join('<br />', $notificationspass), 'notifysuccess');
455}
456if (count($notificationsfail) > 0) {
457 echo $renderer->notification(join('<br />', $notificationsfail));
458}
459
460// Start the management form.
461echo $renderer->management_form_start();
462
463echo $renderer->grid_start('course-category-listings', $class);
464if ($viewmode === 'default' || $viewmode === 'combined' || $viewmode === 'categories') {
465 echo $renderer->grid_column_start($categorysize, 'category-listing');
466 echo $renderer->category_listing($category);
467 echo $renderer->grid_column_end();
468}
469if ($viewmode === 'default' || $viewmode === 'combined' || $viewmode === 'courses') {
470 echo $renderer->grid_column_start($coursesize, 'course-listing');
471 if (!$issearching) {
472 echo $renderer->course_listing($category, $course, $page, $perpage);
473 } else {
474 list($courses, $coursescount, $coursestotal) =
475 \core_course\management\helper::search_courses($search, $blocklist, $modulelist, $page, $perpage);
476 echo $renderer->search_listing($courses, $coursestotal, $course, $page, $perpage);
477 }
478 echo $renderer->grid_column_end();
479 if (isset($courseid)) {
480 echo $renderer->grid_column_start($detailssize, 'course-detail');
481 echo $renderer->course_detail($course);
482 echo $renderer->grid_column_end();
483 }
484}
485echo $renderer->grid_end();
486
5dc361e1
SH
487// End of the management form.
488echo $renderer->management_form_end();
d61c05ac 489echo $renderer->footer();