Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]

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