Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   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 = 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.
 113  require_login();
 114  
 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);
 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  );
 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'));
 146  }
 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      }
 160  }
 161  
 162  $notificationspass = array();
 163  $notificationsfail = array();
 164  
 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()));
 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  
 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);
 428  }
 429  if ((int)$perpage != $perpage || $perpage < 2) {
 430      $perpage = $CFG->coursesperpage;
 431  }
 432  
 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      }
 456  }
 457  if ($viewmode === 'default' || $viewmode === 'combined') {
 458      $class .= ' viewmode-cobmined';
 459  } else {
 460      $class .= ' viewmode-'.$viewmode;
 461  }
 462  if (($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  
 474  echo $renderer->header();
 475  
 476  if (!$issearching) {
 477      echo $renderer->management_heading($strmanagement, $viewmode, $categoryid);
 478  } else {
 479      echo $renderer->management_heading(new lang_string('searchresults'));
 480  }
 481  
 482  if (count($notificationspass) > 0) {
 483      echo $renderer->notification(join('<br />', $notificationspass), 'notifysuccess');
 484  }
 485  if (count($notificationsfail) > 0) {
 486      echo $renderer->notification(join('<br />', $notificationsfail));
 487  }
 488  
 489  // Start the management form.
 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  echo $renderer->course_search_form($search);
 522  
 523  echo $renderer->footer();