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