MDL-69454 core_search: consistent course management search
[moodle.git] / course / management.php
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/>.
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  */
25 require_once('../config.php');
26 require_once($CFG->dirroot.'/course/lib.php');
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.
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.
41 if (!in_array($viewmode, array('default', 'combined', 'courses', 'categories'))) {
42     $viewmode = 'default';
43 }
45 $issearching = ($search !== '' || $blocklist !== 0 || $modulelist !== '');
46 if ($issearching) {
47     $viewmode = 'courses';
48 }
50 $url = new moodle_url('/course/management.php');
51 $systemcontext = $context = context_system::instance();
52 if ($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);
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);
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 }
81 // Check if there is a selected category param, and if there is apply it.
82 if ($course === null && $selectedcategoryid !== null && $selectedcategoryid !== $categoryid) {
83     $url->param('categoryid', $selectedcategoryid);
84 }
86 if ($page !== 0) {
87     $url->param('page', $page);
88 }
89 if ($viewmode !== 'default') {
90     $url->param('view', $viewmode);
91 }
92 if ($search !== '') {
93     $url->param('search', $search);
94 }
95 if ($blocklist !== 0) {
96     $url->param('blocklist', $search);
97 }
98 if ($modulelist !== '') {
99     $url->param('modulelist', $search);
102 $strmanagement = new lang_string('coursecatmanagement');
103 $pageheading = format_string($SITE->fullname, true, array('context' => $systemcontext));
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));
112 // This is a system level page that operates on other contexts.
113 require_login();
115 if (!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);
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 );
134 if ($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'));
147 if (!$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     }
162 $notificationspass = array();
163 $notificationsfail = array();
165 if ($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()));
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);
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);
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                 }
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     }
424 if (!is_null($perpage)) {
425     set_user_preference('coursecat_management_perpage', $perpage);
426 } else {
427     $perpage = get_user_preferences('coursecat_management_perpage', $CFG->coursesperpage);
429 if ((int)$perpage != $perpage || $perpage < 2) {
430     $perpage = $CFG->coursesperpage;
433 $categorysize = 4;
434 $coursesize = 4;
435 $detailssize = 4;
436 if ($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     }
457 if ($viewmode === 'default' || $viewmode === 'combined') {
458     $class .= ' viewmode-cobmined';
459 } else {
460     $class .= ' viewmode-'.$viewmode;
462 if (($viewmode === 'default' || $viewmode === 'combined' || $viewmode === 'courses') && !empty($courseid)) {
463     $class .= ' course-selected';
466 /* @var core_course_management_renderer|core_renderer $renderer */
467 $renderer = $PAGE->get_renderer('core_course', 'management');
468 $renderer->enhance_management_interface();
470 $displaycategorylisting = ($viewmode === 'default' || $viewmode === 'combined' || $viewmode === 'categories');
471 $displaycourselisting = ($viewmode === 'default' || $viewmode === 'combined' || $viewmode === 'courses');
472 $displaycoursedetail = (isset($courseid));
474 echo $renderer->header();
476 if (!$issearching) {
477     echo $renderer->management_heading($strmanagement, $viewmode, $categoryid);
478 } else {
479     echo $renderer->management_heading(new lang_string('searchresults'));
482 if (count($notificationspass) > 0) {
483     echo $renderer->notification(join('<br />', $notificationspass), 'notifysuccess');
485 if (count($notificationsfail) > 0) {
486     echo $renderer->notification(join('<br />', $notificationsfail));
489 // Start the management form.
491 echo $renderer->course_search_form($search);
493 echo $renderer->management_form_start();
495 echo $renderer->accessible_skipto_links($displaycategorylisting, $displaycourselisting, $displaycoursedetail);
497 echo $renderer->grid_start('course-category-listings', $class);
499 if ($displaycategorylisting) {
500     echo $renderer->grid_column_start($categorysize, 'category-listing');
501     echo $renderer->category_listing($category);
502     echo $renderer->grid_column_end();
504 if ($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     }
520 echo $renderer->grid_end();
522 // End of the management form.
523 echo $renderer->management_form_end();
525 echo $renderer->footer();