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   * Contains renderers for the course management pages.
  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  defined('MOODLE_INTERNAL') || die;
  26  
  27  require_once($CFG->dirroot.'/course/renderer.php');
  28  
  29  /**
  30   * Main renderer for the course management pages.
  31   *
  32   * @package core_course
  33   * @copyright 2013 Sam Hemelryk
  34   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class core_course_management_renderer extends plugin_renderer_base {
  37  
  38      /**
  39       * Initialises the JS required to enhance the management interface.
  40       *
  41       * Thunderbirds are go, this function kicks into gear the JS that makes the
  42       * course management pages that much cooler.
  43       */
  44      public function enhance_management_interface() {
  45          $this->page->requires->yui_module('moodle-course-management', 'M.course.management.init');
  46          $this->page->requires->strings_for_js(
  47              array(
  48                  'show',
  49                  'showcategory',
  50                  'hide',
  51                  'expand',
  52                  'expandcategory',
  53                  'collapse',
  54                  'collapsecategory',
  55                  'confirmcoursemove',
  56                  'move',
  57                  'cancel',
  58                  'confirm'
  59              ),
  60              'moodle'
  61          );
  62      }
  63  
  64      /**
  65       * Displays a heading for the management pages.
  66       *
  67       * @deprecated since Moodle 4.0. This is now handled/replaced with the tertiary navigation
  68       * @todo Final deprecation MDL-73975
  69       * @param string $heading The heading to display
  70       * @param string|null $viewmode The current view mode if there are options.
  71       * @param int|null $categoryid The currently selected category if there is one.
  72       * @return string
  73       */
  74      public function management_heading($heading, $viewmode = null, $categoryid = null) {
  75          debugging('management_heading() is deprecated. Use the class manage_categories_action_bar instead.', DEBUG_DEVELOPER);
  76  
  77          $html = html_writer::start_div('coursecat-management-header clearfix');
  78          if (!empty($heading)) {
  79              $html .= $this->heading($heading);
  80          }
  81          if ($viewmode !== null) {
  82              $html .= html_writer::start_div();
  83              $html .= $this->view_mode_selector(\core_course\management\helper::get_management_viewmodes(), $viewmode);
  84              if ($viewmode === 'courses') {
  85                  $categories = core_course_category::make_categories_list(array('moodle/category:manage', 'moodle/course:create'));
  86                  $nothing = false;
  87                  if ($categoryid === null) {
  88                      $nothing = array('' => get_string('selectacategory'));
  89                      $categoryid = '';
  90                  }
  91                  $select = new single_select($this->page->url, 'categoryid', $categories, $categoryid, $nothing);
  92                  $select->attributes['aria-label'] = get_string('selectacategory');
  93                  $html .= $this->render($select);
  94              }
  95              $html .= html_writer::end_div();
  96          }
  97          $html .= html_writer::end_div();
  98          return $html;
  99      }
 100  
 101      /**
 102       * Prepares the form element for the course category listing bulk actions.
 103       *
 104       * @return string
 105       */
 106      public function management_form_start() {
 107          $form = array('action' => $this->page->url->out(), 'method' => 'POST', 'id' => 'coursecat-management');
 108  
 109          $html = html_writer::start_tag('form', $form);
 110          $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
 111          $html .=  html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'action', 'value' => 'bulkaction'));
 112          return $html;
 113      }
 114  
 115      /**
 116       * Closes the course category bulk management form.
 117       *
 118       * @return string
 119       */
 120      public function management_form_end() {
 121          return html_writer::end_tag('form');
 122      }
 123  
 124      /**
 125       * Presents a course category listing.
 126       *
 127       * @param core_course_category $category The currently selected category. Also the category to highlight in the listing.
 128       * @return string
 129       */
 130      public function category_listing(core_course_category $category = null) {
 131  
 132          if ($category === null) {
 133              $selectedparents = array();
 134              $selectedcategory = null;
 135          } else {
 136              $selectedparents = $category->get_parents();
 137              $selectedparents[] = $category->id;
 138              $selectedcategory = $category->id;
 139          }
 140          $catatlevel = \core_course\management\helper::get_expanded_categories('');
 141          $catatlevel[] = array_shift($selectedparents);
 142          $catatlevel = array_unique($catatlevel);
 143  
 144          $listing = core_course_category::top()->get_children();
 145  
 146          $attributes = array(
 147                  'class' => 'ml-1 list-unstyled',
 148                  'role' => 'tree',
 149                  'aria-labelledby' => 'category-listing-title'
 150          );
 151  
 152          $html  = html_writer::start_div('category-listing card w-100');
 153          $html .= html_writer::tag('h3', get_string('categories'),
 154                  array('class' => 'card-header', 'id' => 'category-listing-title'));
 155          $html .= html_writer::start_div('card-body');
 156          $html .= $this->category_listing_actions($category);
 157          $html .= html_writer::start_tag('ul', $attributes);
 158          foreach ($listing as $listitem) {
 159              // Render each category in the listing.
 160              $subcategories = array();
 161              if (in_array($listitem->id, $catatlevel)) {
 162                  $subcategories = $listitem->get_children();
 163              }
 164              $html .= $this->category_listitem(
 165                      $listitem,
 166                      $subcategories,
 167                      $listitem->get_children_count(),
 168                      $selectedcategory,
 169                      $selectedparents
 170              );
 171          }
 172          $html .= html_writer::end_tag('ul');
 173          $html .= $this->category_bulk_actions($category);
 174          $html .= html_writer::end_div();
 175          $html .= html_writer::end_div();
 176          return $html;
 177      }
 178  
 179      /**
 180       * Renders a category list item.
 181       *
 182       * This function gets called recursively to render sub categories.
 183       *
 184       * @param core_course_category $category The category to render as listitem.
 185       * @param core_course_category[] $subcategories The subcategories belonging to the category being rented.
 186       * @param int $totalsubcategories The total number of sub categories.
 187       * @param int $selectedcategory The currently selected category
 188       * @param int[] $selectedcategories The path to the selected category and its ID.
 189       * @return string
 190       */
 191      public function category_listitem(core_course_category $category, array $subcategories, $totalsubcategories,
 192              $selectedcategory = null, $selectedcategories = array()) {
 193  
 194          $isexpandable = ($totalsubcategories > 0);
 195          $isexpanded = (!empty($subcategories));
 196          $activecategory = ($selectedcategory === $category->id);
 197          $attributes = array(
 198                  'class' => 'listitem listitem-category list-group-item list-group-item-action',
 199                  'data-id' => $category->id,
 200                  'data-expandable' => $isexpandable ? '1' : '0',
 201                  'data-expanded' => $isexpanded ? '1' : '0',
 202                  'data-selected' => $activecategory ? '1' : '0',
 203                  'data-visible' => $category->visible ? '1' : '0',
 204                  'role' => 'treeitem',
 205                  'aria-expanded' => $isexpanded ? 'true' : 'false'
 206          );
 207          $text = $category->get_formatted_name();
 208          if (($parent = $category->get_parent_coursecat()) && $parent->id) {
 209              $a = new stdClass;
 210              $a->category = $text;
 211              $a->parentcategory = $parent->get_formatted_name();
 212              $textlabel = get_string('categorysubcategoryof', 'moodle', $a);
 213          }
 214          $courseicon = $this->output->pix_icon('i/course', get_string('courses'));
 215          $bcatinput = array(
 216                  'id' => 'categorylistitem' . $category->id,
 217                  'type' => 'checkbox',
 218                  'name' => 'bcat[]',
 219                  'value' => $category->id,
 220                  'class' => 'bulk-action-checkbox custom-control-input',
 221                  'data-action' => 'select'
 222          );
 223  
 224          $checkboxclass = '';
 225          if (!$category->can_resort_subcategories() && !$category->has_manage_capability()) {
 226              // Very very hardcoded here.
 227              $checkboxclass = 'd-none';
 228          }
 229  
 230          $viewcaturl = new moodle_url('/course/management.php', array('categoryid' => $category->id));
 231          if ($isexpanded) {
 232              $icon = $this->output->pix_icon('t/switch_minus', get_string('collapse'),
 233                      'moodle', array('class' => 'tree-icon', 'title' => ''));
 234              $icon = html_writer::link(
 235                      $viewcaturl,
 236                      $icon,
 237                      array(
 238                              'class' => 'float-left',
 239                              'data-action' => 'collapse',
 240                              'title' => get_string('collapsecategory', 'moodle', $text),
 241                              'aria-controls' => 'subcategoryof'.$category->id
 242                      )
 243              );
 244          } else if ($isexpandable) {
 245              $icon = $this->output->pix_icon('t/switch_plus', get_string('expand'),
 246                      'moodle', array('class' => 'tree-icon', 'title' => ''));
 247              $icon = html_writer::link(
 248                      $viewcaturl,
 249                      $icon,
 250                      array(
 251                              'class' => 'float-left',
 252                              'data-action' => 'expand',
 253                              'title' => get_string('expandcategory', 'moodle', $text)
 254                      )
 255              );
 256          } else {
 257              $icon = $this->output->pix_icon(
 258                      'i/empty',
 259                      '',
 260                      'moodle',
 261                      array('class' => 'tree-icon'));
 262              $icon = html_writer::span($icon, 'float-left');
 263          }
 264          $actions = \core_course\management\helper::get_category_listitem_actions($category);
 265          $hasactions = !empty($actions) || $category->can_create_course();
 266  
 267          $html = html_writer::start_tag('li', $attributes);
 268          $html .= html_writer::start_div('clearfix');
 269          $html .= html_writer::start_div('float-left ' . $checkboxclass);
 270          $html .= html_writer::start_div('custom-control custom-checkbox mr-1 ');
 271          $html .= html_writer::empty_tag('input', $bcatinput);
 272          $labeltext = html_writer::span(get_string('bulkactionselect', 'moodle', $text), 'sr-only');
 273          $html .= html_writer::tag('label', $labeltext, array(
 274              'class' => 'custom-control-label',
 275              'for' => 'categorylistitem' . $category->id));
 276          $html .= html_writer::end_div();
 277          $html .= html_writer::end_div();
 278          $html .= $icon;
 279          if ($hasactions) {
 280              $textattributes = array('class' => 'float-left categoryname aalink');
 281          } else {
 282              $textattributes = array('class' => 'float-left categoryname without-actions');
 283          }
 284          if (isset($textlabel)) {
 285              $textattributes['aria-label'] = $textlabel;
 286          }
 287          $html .= html_writer::link($viewcaturl, $text, $textattributes);
 288          $html .= html_writer::start_div('float-right d-flex');
 289          if ($category->idnumber) {
 290              $html .= html_writer::tag('span', s($category->idnumber), array('class' => 'text-muted idnumber'));
 291          }
 292          if ($hasactions) {
 293              $html .= $this->category_listitem_actions($category, $actions);
 294          }
 295          $countid = 'course-count-'.$category->id;
 296          $html .= html_writer::span(
 297                  html_writer::span($category->get_courses_count()) .
 298                  html_writer::span(get_string('courses'), 'accesshide', array('id' => $countid)) .
 299                  $courseicon,
 300                  'course-count text-muted',
 301                  array('aria-labelledby' => $countid)
 302          );
 303          $html .= html_writer::end_div();
 304          $html .= html_writer::end_div();
 305          if ($isexpanded) {
 306              $html .= html_writer::start_tag('ul',
 307                      array('class' => 'ml', 'role' => 'group', 'id' => 'subcategoryof'.$category->id));
 308              $catatlevel = \core_course\management\helper::get_expanded_categories($category->path);
 309              $catatlevel[] = array_shift($selectedcategories);
 310              $catatlevel = array_unique($catatlevel);
 311              foreach ($subcategories as $listitem) {
 312                  $childcategories = (in_array($listitem->id, $catatlevel)) ? $listitem->get_children() : array();
 313                  $html .= $this->category_listitem(
 314                          $listitem,
 315                          $childcategories,
 316                          $listitem->get_children_count(),
 317                          $selectedcategory,
 318                          $selectedcategories
 319                  );
 320              }
 321              $html .= html_writer::end_tag('ul');
 322          }
 323          $html .= html_writer::end_tag('li');
 324          return $html;
 325      }
 326  
 327      /**
 328       * Renderers the actions that are possible for the course category listing.
 329       *
 330       * These are not the actions associated with an individual category listing.
 331       * That happens through category_listitem_actions.
 332       *
 333       * @param core_course_category $category
 334       * @return string
 335       */
 336      public function category_listing_actions(core_course_category $category = null) {
 337          $actions = array();
 338  
 339          $cancreatecategory = $category && $category->can_create_subcategory();
 340          $cancreatecategory = $cancreatecategory || core_course_category::can_create_top_level_category();
 341          if ($category === null) {
 342              $category = core_course_category::top();
 343          }
 344  
 345          if ($cancreatecategory) {
 346              $url = new moodle_url('/course/editcategory.php', array('parent' => $category->id));
 347              $actions[] = html_writer::link($url, get_string('createnewcategory'), array('class' => 'btn btn-secondary'));
 348          }
 349          if (core_course_category::can_approve_course_requests()) {
 350              $actions[] = html_writer::link(new moodle_url('/course/pending.php'), get_string('coursespending'));
 351          }
 352          if (count($actions) === 0) {
 353              return '';
 354          }
 355          return html_writer::div(join(' ', $actions), 'listing-actions category-listing-actions mb-3');
 356      }
 357  
 358      /**
 359       * Renderers the actions for individual category list items.
 360       *
 361       * @param core_course_category $category
 362       * @param array $actions
 363       * @return string
 364       */
 365      public function category_listitem_actions(core_course_category $category, array $actions = null) {
 366          if ($actions === null) {
 367              $actions = \core_course\management\helper::get_category_listitem_actions($category);
 368          }
 369          $menu = new action_menu();
 370          $menu->attributes['class'] .= ' category-item-actions item-actions';
 371          $hasitems = false;
 372          foreach ($actions as $key => $action) {
 373              $hasitems = true;
 374              $menu->add(new action_menu_link(
 375                  $action['url'],
 376                  $action['icon'],
 377                  $action['string'],
 378                  in_array($key, array('show', 'hide', 'moveup', 'movedown')),
 379                  array('data-action' => $key, 'class' => 'action-'.$key)
 380              ));
 381          }
 382          if (!$hasitems) {
 383              return '';
 384          }
 385  
 386          // If the action menu has items, add the menubar role to the main element containing it.
 387          $menu->attributes['role'] = 'menubar';
 388  
 389          return $this->render($menu);
 390      }
 391  
 392      public function render_action_menu($menu) {
 393          return $this->output->render($menu);
 394      }
 395  
 396      /**
 397       * Renders bulk actions for categories.
 398       *
 399       * @param core_course_category $category The currently selected category if there is one.
 400       * @return string
 401       */
 402      public function category_bulk_actions(core_course_category $category = null) {
 403          // Resort courses.
 404          // Change parent.
 405          if (!core_course_category::can_resort_any() && !core_course_category::can_change_parent_any()) {
 406              return '';
 407          }
 408          $strgo = new lang_string('go');
 409  
 410          $html  = html_writer::start_div('category-bulk-actions bulk-actions');
 411          $html .= html_writer::div(get_string('categorybulkaction'), 'accesshide', array('tabindex' => '0'));
 412          if (core_course_category::can_resort_any()) {
 413              $selectoptions = array(
 414                  'selectedcategories' => get_string('selectedcategories'),
 415                  'allcategories' => get_string('allcategories')
 416              );
 417              $form = html_writer::start_div();
 418              if ($category) {
 419                  $selectoptions = array('thiscategory' => get_string('thiscategory')) + $selectoptions;
 420                  $form .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'currentcategoryid', 'value' => $category->id));
 421              }
 422              $form .= html_writer::div(
 423                  html_writer::select(
 424                      $selectoptions,
 425                      'selectsortby',
 426                      'selectedcategories',
 427                      false,
 428                      array('aria-label' => get_string('selectcategorysort'))
 429                  )
 430              );
 431              $form .= html_writer::div(
 432                  html_writer::select(
 433                      array(
 434                          'name' => get_string('sortbyx', 'moodle', get_string('categoryname')),
 435                          'namedesc' => get_string('sortbyxreverse', 'moodle', get_string('categoryname')),
 436                          'idnumber' => get_string('sortbyx', 'moodle', get_string('idnumbercoursecategory')),
 437                          'idnumberdesc' => get_string('sortbyxreverse' , 'moodle' , get_string('idnumbercoursecategory')),
 438                          'none' => get_string('dontsortcategories')
 439                      ),
 440                      'resortcategoriesby',
 441                      'name',
 442                      false,
 443                      array('aria-label' => get_string('selectcategorysortby'), 'class' => 'mt-1')
 444                  )
 445              );
 446              $form .= html_writer::div(
 447                  html_writer::select(
 448                      array(
 449                          'fullname' => get_string('sortbyx', 'moodle', get_string('fullnamecourse')),
 450                          'fullnamedesc' => get_string('sortbyxreverse', 'moodle', get_string('fullnamecourse')),
 451                          'shortname' => get_string('sortbyx', 'moodle', get_string('shortnamecourse')),
 452                          'shortnamedesc' => get_string('sortbyxreverse', 'moodle', get_string('shortnamecourse')),
 453                          'idnumber' => get_string('sortbyx', 'moodle', get_string('idnumbercourse')),
 454                          'idnumberdesc' => get_string('sortbyxreverse', 'moodle', get_string('idnumbercourse')),
 455                          'timecreated' => get_string('sortbyx', 'moodle', get_string('timecreatedcourse')),
 456                          'timecreateddesc' => get_string('sortbyxreverse', 'moodle', get_string('timecreatedcourse')),
 457                          'none' => get_string('dontsortcourses')
 458                      ),
 459                      'resortcoursesby',
 460                      'fullname',
 461                      false,
 462                      array('aria-label' => get_string('selectcoursesortby'), 'class' => 'mt-1')
 463                  )
 464              );
 465              $form .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'bulksort',
 466                  'value' => get_string('sort'), 'class' => 'btn btn-secondary my-1'));
 467              $form .= html_writer::end_div();
 468  
 469              $html .= html_writer::start_div('detail-pair row yui3-g my-1');
 470              $html .= html_writer::div(html_writer::span(get_string('sorting')), 'pair-key col-md-3 yui3-u-1-4');
 471              $html .= html_writer::div($form, 'pair-value col-md-9 yui3-u-3-4');
 472              $html .= html_writer::end_div();
 473          }
 474          if (core_course_category::can_change_parent_any()) {
 475              $options = array();
 476              if (core_course_category::top()->has_manage_capability()) {
 477                  $options[0] = core_course_category::top()->get_formatted_name();
 478              }
 479              $options += core_course_category::make_categories_list('moodle/category:manage');
 480              $select = html_writer::select(
 481                  $options,
 482                  'movecategoriesto',
 483                  '',
 484                  array('' => 'choosedots'),
 485                  array('aria-labelledby' => 'moveselectedcategoriesto', 'class' => 'mr-1')
 486              );
 487              $submit = array('type' => 'submit', 'name' => 'bulkmovecategories', 'value' => get_string('move'),
 488                  'class' => 'btn btn-secondary');
 489              $html .= $this->detail_pair(
 490                  html_writer::span(get_string('moveselectedcategoriesto'), '', array('id' => 'moveselectedcategoriesto')),
 491                  $select . html_writer::empty_tag('input', $submit)
 492              );
 493          }
 494          $html .= html_writer::end_div();
 495          return $html;
 496      }
 497  
 498      /**
 499       * Renders a course listing.
 500       *
 501       * @param core_course_category $category The currently selected category. This is what the listing is focused on.
 502       * @param core_course_list_element $course The currently selected course.
 503       * @param int $page The page being displayed.
 504       * @param int $perpage The number of courses to display per page.
 505       * @param string|null $viewmode The view mode the page is in, one out of 'default', 'combined', 'courses' or 'categories'.
 506       * @return string
 507       */
 508      public function course_listing(core_course_category $category = null, core_course_list_element $course = null,
 509              $page = 0, $perpage = 20, $viewmode = 'default') {
 510  
 511          if ($category === null) {
 512              $html = html_writer::start_div('select-a-category');
 513              $html .= html_writer::tag('h3', get_string('courses'),
 514                      array('id' => 'course-listing-title', 'tabindex' => '0'));
 515              $html .= $this->output->notification(get_string('selectacategory'), 'notifymessage');
 516              $html .= html_writer::end_div();
 517              return $html;
 518          }
 519  
 520          $page = max($page, 0);
 521          $perpage = max($perpage, 2);
 522          $totalcourses = $category->coursecount;
 523          $totalpages = ceil($totalcourses / $perpage);
 524          if ($page > $totalpages - 1) {
 525              $page = $totalpages - 1;
 526          }
 527          $options = array(
 528                  'offset' => $page * $perpage,
 529                  'limit' => $perpage
 530          );
 531          $courseid = isset($course) ? $course->id : null;
 532          $class = '';
 533          if ($page === 0) {
 534              $class .= ' firstpage';
 535          }
 536          if ($page + 1 === (int)$totalpages) {
 537              $class .= ' lastpage';
 538          }
 539  
 540          $html  = html_writer::start_div('card course-listing w-100'.$class, array(
 541                  'data-category' => $category->id,
 542                  'data-page' => $page,
 543                  'data-totalpages' => $totalpages,
 544                  'data-totalcourses' => $totalcourses,
 545                  'data-canmoveoutof' => $category->can_move_courses_out_of() && $category->can_move_courses_into()
 546          ));
 547          $html .= html_writer::tag('h3', $category->get_formatted_name(),
 548                  array('id' => 'course-listing-title', 'tabindex' => '0', 'class' => 'card-header'));
 549          $html .= html_writer::start_div('card-body');
 550          $html .= $this->course_listing_actions($category, $course, $perpage);
 551          $html .= $this->listing_pagination($category, $page, $perpage, false, $viewmode);
 552          $html .= html_writer::start_tag('ul', array('class' => 'ml course-list'));
 553          foreach ($category->get_courses($options) as $listitem) {
 554              $html .= $this->course_listitem($category, $listitem, $courseid);
 555          }
 556          $html .= html_writer::end_tag('ul');
 557          $html .= $this->listing_pagination($category, $page, $perpage, true, $viewmode);
 558          $html .= $this->course_bulk_actions($category);
 559          $html .= html_writer::end_div();
 560          $html .= html_writer::end_div();
 561          return $html;
 562      }
 563  
 564      /**
 565       * Renders pagination for a course listing.
 566       *
 567       * @param core_course_category $category The category to produce pagination for.
 568       * @param int $page The current page.
 569       * @param int $perpage The number of courses to display per page.
 570       * @param bool $showtotals Set to true to show the total number of courses and what is being displayed.
 571       * @param string|null $viewmode The view mode the page is in, one out of 'default', 'combined', 'courses' or 'categories'.
 572       * @return string
 573       */
 574      protected function listing_pagination(core_course_category $category, $page, $perpage, $showtotals = false,
 575                                            $viewmode = 'default') {
 576          $html = '';
 577          $totalcourses = $category->get_courses_count();
 578          $totalpages = ceil($totalcourses / $perpage);
 579          if ($showtotals) {
 580              if ($totalpages == 0) {
 581                  $str = get_string('nocoursesyet');
 582              } else if ($totalpages == 1) {
 583                  $str = get_string('showingacourses', 'moodle', $totalcourses);
 584              } else {
 585                  $a = new stdClass;
 586                  $a->start = ($page * $perpage) + 1;
 587                  $a->end = min((($page + 1) * $perpage), $totalcourses);
 588                  $a->total = $totalcourses;
 589                  $str = get_string('showingxofycourses', 'moodle', $a);
 590              }
 591              $html .= html_writer::div($str, 'listing-pagination-totals text-muted');
 592          }
 593  
 594          if ($viewmode !== 'default') {
 595              $baseurl = new moodle_url('/course/management.php', array('categoryid' => $category->id,
 596                  'view' => $viewmode));
 597          } else {
 598              $baseurl = new moodle_url('/course/management.php', array('categoryid' => $category->id));
 599          }
 600  
 601          $html .= $this->output->paging_bar($totalcourses, $page, $perpage, $baseurl);
 602          return $html;
 603      }
 604  
 605      /**
 606       * Renderers a course list item.
 607       *
 608       * This function will be called for every course being displayed by course_listing.
 609       *
 610       * @param core_course_category $category The currently selected category and the category the course belongs to.
 611       * @param core_course_list_element $course The course to produce HTML for.
 612       * @param int $selectedcourse The id of the currently selected course.
 613       * @return string
 614       */
 615      public function course_listitem(core_course_category $category, core_course_list_element $course, $selectedcourse) {
 616  
 617          $text = $course->get_formatted_name();
 618          $attributes = array(
 619                  'class' => 'listitem listitem-course list-group-item list-group-item-action',
 620                  'data-id' => $course->id,
 621                  'data-selected' => ($selectedcourse == $course->id) ? '1' : '0',
 622                  'data-visible' => $course->visible ? '1' : '0'
 623          );
 624  
 625          $bulkcourseinput = array(
 626                  'id' => 'courselistitem' . $course->id,
 627                  'type' => 'checkbox',
 628                  'name' => 'bc[]',
 629                  'value' => $course->id,
 630                  'class' => 'bulk-action-checkbox custom-control-input',
 631                  'data-action' => 'select'
 632          );
 633  
 634          $checkboxclass = '';
 635          if (!$category->has_manage_capability()) {
 636              // Very very hardcoded here.
 637              $checkboxclass = 'd-none';
 638          }
 639  
 640          $viewcourseurl = new moodle_url($this->page->url, array('courseid' => $course->id));
 641  
 642          $html  = html_writer::start_tag('li', $attributes);
 643          $html .= html_writer::start_div('d-flex flex-wrap');
 644  
 645          if ($category->can_resort_courses()) {
 646              // In order for dnd to be available the user must be able to resort the category children..
 647              $html .= html_writer::div($this->output->pix_icon('i/move_2d', get_string('dndcourse')), 'float-left drag-handle');
 648          }
 649  
 650          $html .= html_writer::start_div('float-left ' . $checkboxclass);
 651          $html .= html_writer::start_div('custom-control custom-checkbox mr-1 ');
 652          $html .= html_writer::empty_tag('input', $bulkcourseinput);
 653          $labeltext = html_writer::span(get_string('bulkactionselect', 'moodle', $text), 'sr-only');
 654          $html .= html_writer::tag('label', $labeltext, array(
 655              'class' => 'custom-control-label',
 656              'for' => 'courselistitem' . $course->id));
 657          $html .= html_writer::end_div();
 658          $html .= html_writer::end_div();
 659          $html .= html_writer::link(
 660              $viewcourseurl, $text, array('class' => 'text-break col pl-0 mb-2 coursename aalink')
 661          );
 662          $html .= html_writer::start_div('flex-shrink-0 ml-auto');
 663          if ($course->idnumber) {
 664              $html .= html_writer::tag('span', s($course->idnumber), array('class' => 'text-muted idnumber'));
 665          }
 666          $html .= $this->course_listitem_actions($category, $course);
 667          $html .= html_writer::end_div();
 668          $html .= html_writer::end_div();
 669          $html .= html_writer::end_tag('li');
 670          return $html;
 671      }
 672  
 673      /**
 674       * Renderers actions for the course listing.
 675       *
 676       * Not to be confused with course_listitem_actions which renderers the actions for individual courses.
 677       *
 678       * @param core_course_category $category
 679       * @param core_course_list_element $course The currently selected course.
 680       * @param int $perpage
 681       * @return string
 682       */
 683      public function course_listing_actions(core_course_category $category, core_course_list_element $course = null, $perpage = 20) {
 684          $actions = array();
 685          if ($category->can_create_course()) {
 686              $url = new moodle_url('/course/edit.php', array('category' => $category->id, 'returnto' => 'catmanage'));
 687              $actions[] = html_writer::link($url, get_string('createnewcourse'), array('class' => 'btn btn-secondary'));
 688          }
 689          if ($category->can_request_course()) {
 690              // Request a new course.
 691              $url = new moodle_url('/course/request.php', array('category' => $category->id, 'return' => 'management'));
 692              $actions[] = html_writer::link($url, get_string('requestcourse'));
 693          }
 694          if ($category->can_resort_courses()) {
 695              $params = $this->page->url->params();
 696              $params['action'] = 'resortcourses';
 697              $params['sesskey'] = sesskey();
 698              $baseurl = new moodle_url('/course/management.php', $params);
 699              $fullnameurl = new moodle_url($baseurl, array('resort' => 'fullname'));
 700              $fullnameurldesc = new moodle_url($baseurl, array('resort' => 'fullnamedesc'));
 701              $shortnameurl = new moodle_url($baseurl, array('resort' => 'shortname'));
 702              $shortnameurldesc = new moodle_url($baseurl, array('resort' => 'shortnamedesc'));
 703              $idnumberurl = new moodle_url($baseurl, array('resort' => 'idnumber'));
 704              $idnumberdescurl = new moodle_url($baseurl, array('resort' => 'idnumberdesc'));
 705              $timecreatedurl = new moodle_url($baseurl, array('resort' => 'timecreated'));
 706              $timecreateddescurl = new moodle_url($baseurl, array('resort' => 'timecreateddesc'));
 707              $menu = new action_menu(array(
 708                      new action_menu_link_secondary($fullnameurl,
 709                              null,
 710                              get_string('sortbyx', 'moodle', get_string('fullnamecourse'))),
 711                      new action_menu_link_secondary($fullnameurldesc,
 712                              null,
 713                              get_string('sortbyxreverse', 'moodle', get_string('fullnamecourse'))),
 714                      new action_menu_link_secondary($shortnameurl,
 715                              null,
 716                              get_string('sortbyx', 'moodle', get_string('shortnamecourse'))),
 717                      new action_menu_link_secondary($shortnameurldesc,
 718                              null,
 719                              get_string('sortbyxreverse', 'moodle', get_string('shortnamecourse'))),
 720                      new action_menu_link_secondary($idnumberurl,
 721                              null,
 722                              get_string('sortbyx', 'moodle', get_string('idnumbercourse'))),
 723                      new action_menu_link_secondary($idnumberdescurl,
 724                              null,
 725                              get_string('sortbyxreverse', 'moodle', get_string('idnumbercourse'))),
 726                      new action_menu_link_secondary($timecreatedurl,
 727                              null,
 728                              get_string('sortbyx', 'moodle', get_string('timecreatedcourse'))),
 729                      new action_menu_link_secondary($timecreateddescurl,
 730                              null,
 731                              get_string('sortbyxreverse', 'moodle', get_string('timecreatedcourse')))
 732              ));
 733              $menu->set_menu_trigger(get_string('resortcourses'));
 734              $actions[] = $this->render($menu);
 735          }
 736          $strall = get_string('all');
 737          $menu = new action_menu(array(
 738                  new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 5)), null, 5),
 739                  new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 10)), null, 10),
 740                  new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 20)), null, 20),
 741                  new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 50)), null, 50),
 742                  new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 100)), null, 100),
 743                  new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 999)), null, $strall),
 744          ));
 745          if ((int)$perpage === 999) {
 746              $perpage = $strall;
 747          }
 748          $menu->attributes['class'] .= ' courses-per-page';
 749          $menu->set_menu_trigger(get_string('perpagea', 'moodle', $perpage));
 750          $actions[] = $this->render($menu);
 751          return html_writer::div(join(' ', $actions), 'listing-actions course-listing-actions');
 752      }
 753  
 754      /**
 755       * Renderers actions for individual course actions.
 756       *
 757       * @param core_course_category $category The currently selected category.
 758       * @param core_course_list_element  $course The course to renderer actions for.
 759       * @return string
 760       */
 761      public function course_listitem_actions(core_course_category $category, core_course_list_element $course) {
 762          $actions = \core_course\management\helper::get_course_listitem_actions($category, $course);
 763          if (empty($actions)) {
 764              return '';
 765          }
 766          $actionshtml = array();
 767          foreach ($actions as $action) {
 768              $action['attributes']['role'] = 'button';
 769              $actionshtml[] = $this->output->action_icon($action['url'], $action['icon'], null, $action['attributes']);
 770          }
 771          return html_writer::span(join('', $actionshtml), 'course-item-actions item-actions mr-0');
 772      }
 773  
 774      /**
 775       * Renderers bulk actions that can be performed on courses.
 776       *
 777       * @param core_course_category $category The currently selected category and the category in which courses that
 778       *      are selectable belong.
 779       * @return string
 780       */
 781      public function course_bulk_actions(core_course_category $category) {
 782          $html  = html_writer::start_div('course-bulk-actions bulk-actions');
 783          if ($category->can_move_courses_out_of()) {
 784              $html .= html_writer::div(get_string('coursebulkaction'), 'accesshide', array('tabindex' => '0'));
 785              $options = core_course_category::make_categories_list('moodle/category:manage');
 786              $select = html_writer::select(
 787                  $options,
 788                  'movecoursesto',
 789                  '',
 790                  array('' => 'choosedots'),
 791                  array('aria-labelledby' => 'moveselectedcoursesto', 'class' => 'mr-1')
 792              );
 793              $submit = array('type' => 'submit', 'name' => 'bulkmovecourses', 'value' => get_string('move'),
 794                  'class' => 'btn btn-secondary');
 795              $html .= $this->detail_pair(
 796                  html_writer::span(get_string('moveselectedcoursesto'), '', array('id' => 'moveselectedcoursesto')),
 797                  $select . html_writer::empty_tag('input', $submit)
 798              );
 799          }
 800          $html .= html_writer::end_div();
 801          return $html;
 802      }
 803  
 804      /**
 805       * Renderers bulk actions that can be performed on courses in search returns
 806       *
 807       * @return string
 808       */
 809      public function course_search_bulk_actions() {
 810          $html  = html_writer::start_div('course-bulk-actions bulk-actions');
 811          $html .= html_writer::div(get_string('coursebulkaction'), 'accesshide', array('tabindex' => '0'));
 812          $options = core_course_category::make_categories_list('moodle/category:manage');
 813          $select = html_writer::select(
 814              $options,
 815              'movecoursesto',
 816              '',
 817              array('' => 'choosedots'),
 818              array('aria-labelledby' => 'moveselectedcoursesto')
 819          );
 820          $submit = array('type' => 'submit', 'name' => 'bulkmovecourses', 'value' => get_string('move'),
 821              'class' => 'btn btn-secondary');
 822          $html .= $this->detail_pair(
 823              html_writer::span(get_string('moveselectedcoursesto'), '', array('id' => 'moveselectedcoursesto')),
 824              $select . html_writer::empty_tag('input', $submit)
 825          );
 826          $html .= html_writer::end_div();
 827          return $html;
 828      }
 829  
 830      /**
 831       * Renderers detailed course information.
 832       *
 833       * @param core_course_list_element $course The course to display details for.
 834       * @return string
 835       */
 836      public function course_detail(core_course_list_element $course) {
 837          $details = \core_course\management\helper::get_course_detail_array($course);
 838          $fullname = $details['fullname']['value'];
 839  
 840          $html = html_writer::start_div('course-detail card');
 841          $html .= html_writer::start_div('card-header');
 842          $html .= html_writer::tag('h3', $fullname, array('id' => 'course-detail-title',
 843                  'class' => 'card-title', 'tabindex' => '0'));
 844          $html .= html_writer::end_div();
 845          $html .= html_writer::start_div('card-body');
 846          $html .= $this->course_detail_actions($course);
 847          foreach ($details as $class => $data) {
 848              $html .= $this->detail_pair($data['key'], $data['value'], $class);
 849          }
 850          $html .= html_writer::end_div();
 851          $html .= html_writer::end_div();
 852          return $html;
 853      }
 854  
 855      /**
 856       * Renderers a key value pair of information for display.
 857       *
 858       * @param string $key
 859       * @param string $value
 860       * @param string $class
 861       * @return string
 862       */
 863      protected function detail_pair($key, $value, $class ='') {
 864          $html = html_writer::start_div('detail-pair row yui3-g '.preg_replace('#[^a-zA-Z0-9_\-]#', '-', $class));
 865          $html .= html_writer::div(html_writer::span($key), 'pair-key col-md-3 yui3-u-1-4 font-weight-bold');
 866          $html .= html_writer::div(html_writer::span($value), 'pair-value col-md-8 yui3-u-3-4');
 867          $html .= html_writer::end_div();
 868          return $html;
 869      }
 870  
 871      /**
 872       * A collection of actions for a course.
 873       *
 874       * @param core_course_list_element $course The course to display actions for.
 875       * @return string
 876       */
 877      public function course_detail_actions(core_course_list_element $course) {
 878          $actions = \core_course\management\helper::get_course_detail_actions($course);
 879          if (empty($actions)) {
 880              return '';
 881          }
 882          $options = array();
 883          foreach ($actions as $action) {
 884              $options[] = $this->action_link($action['url'], $action['string'], null,
 885                      array('class' => 'btn btn-sm btn-secondary mr-1 mb-3'));
 886          }
 887          return html_writer::div(join('', $options), 'listing-actions course-detail-listing-actions');
 888      }
 889  
 890      /**
 891       * Creates an action button (styled link)
 892       *
 893       * @param moodle_url $url The URL to go to when clicked.
 894       * @param string $text The text for the button.
 895       * @param string $id An id to give the button.
 896       * @param string $class A class to give the button.
 897       * @param array $attributes Any additional attributes
 898       * @return string
 899       */
 900      protected function action_button(moodle_url $url, $text, $id = null, $class = null, $title = null, array $attributes = array()) {
 901          if (isset($attributes['class'])) {
 902              $attributes['class'] .= ' yui3-button';
 903          } else {
 904              $attributes['class'] = 'yui3-button';
 905          }
 906          if (!is_null($id)) {
 907              $attributes['id'] = $id;
 908          }
 909          if (!is_null($class)) {
 910              $attributes['class'] .= ' '.$class;
 911          }
 912          if (is_null($title)) {
 913              $title = $text;
 914          }
 915          $attributes['title'] = $title;
 916          if (!isset($attributes['role'])) {
 917              $attributes['role'] = 'button';
 918          }
 919          return html_writer::link($url, $text, $attributes);
 920      }
 921  
 922      /**
 923       * Opens a grid.
 924       *
 925       * Call {@link core_course_management_renderer::grid_column_start()} to create columns.
 926       *
 927       * @param string $id An id to give this grid.
 928       * @param string $class A class to give this grid.
 929       * @return string
 930       */
 931      public function grid_start($id = null, $class = null) {
 932          $gridclass = 'grid-start grid-row-r d-flex flex-wrap row';
 933          if (is_null($class)) {
 934              $class = $gridclass;
 935          } else {
 936              $class .= ' ' . $gridclass;
 937          }
 938          $attributes = array();
 939          if (!is_null($id)) {
 940              $attributes['id'] = $id;
 941          }
 942          return html_writer::start_div($class, $attributes);
 943      }
 944  
 945      /**
 946       * Closes the grid.
 947       *
 948       * @return string
 949       */
 950      public function grid_end() {
 951          return html_writer::end_div();
 952      }
 953  
 954      /**
 955       * Opens a grid column
 956       *
 957       * @param int $size The number of segments this column should span.
 958       * @param string $id An id to give the column.
 959       * @param string $class A class to give the column.
 960       * @return string
 961       */
 962      public function grid_column_start($size, $id = null, $class = null) {
 963  
 964          if ($id == 'course-detail') {
 965              $size = 12;
 966              $bootstrapclass = 'col-md-'.$size;
 967          } else {
 968              $bootstrapclass = 'd-flex flex-wrap px-3 mb-3';
 969          }
 970  
 971          $yuigridclass = "col-sm";
 972          if (in_array($size, [4, 5, 7])) {
 973              $yuigridclass = "col-12 col-lg-6";
 974          }
 975  
 976          if (is_null($class)) {
 977              $class = $yuigridclass . ' ' . $bootstrapclass;
 978          } else {
 979              $class .= ' ' . $yuigridclass . ' ' . $bootstrapclass;
 980          }
 981          $attributes = array();
 982          if (!is_null($id)) {
 983              $attributes['id'] = $id;
 984          }
 985          return html_writer::start_div($class . " grid_column_start", $attributes);
 986      }
 987  
 988      /**
 989       * Closes a grid column.
 990       *
 991       * @return string
 992       */
 993      public function grid_column_end() {
 994          return html_writer::end_div();
 995      }
 996  
 997      /**
 998       * Renders an action_icon.
 999       *
1000       * This function uses the {@link core_renderer::action_link()} method for the
1001       * most part. What it does different is prepare the icon as HTML and use it
1002       * as the link text.
1003       *
1004       * @param string|moodle_url $url A string URL or moodel_url
1005       * @param pix_icon $pixicon
1006       * @param component_action $action
1007       * @param array $attributes associative array of html link attributes + disabled
1008       * @param bool $linktext show title next to image in link
1009       * @return string HTML fragment
1010       */
1011      public function action_icon($url, pix_icon $pixicon, component_action $action = null,
1012                                  array $attributes = null, $linktext = false) {
1013          if (!($url instanceof moodle_url)) {
1014              $url = new moodle_url($url);
1015          }
1016          $attributes = (array)$attributes;
1017  
1018          if (empty($attributes['class'])) {
1019              // Let devs override the class via $attributes.
1020              $attributes['class'] = 'action-icon';
1021          }
1022  
1023          $icon = $this->render($pixicon);
1024  
1025          if ($linktext) {
1026              $text = $pixicon->attributes['alt'];
1027          } else {
1028              $text = '';
1029          }
1030  
1031          return $this->action_link($url, $icon.$text, $action, $attributes);
1032      }
1033  
1034      /**
1035       * Displays a view mode selector.
1036       *
1037       * @param array $modes An array of view modes.
1038       * @param string $currentmode The current view mode.
1039       * @param moodle_url $url The URL to use when changing actions. Defaults to the page URL.
1040       * @param string $param The param name.
1041       * @return string
1042       */
1043      public function view_mode_selector(array $modes, $currentmode, moodle_url $url = null, $param = 'view') {
1044          if ($url === null) {
1045              $url = $this->page->url;
1046          }
1047  
1048          $menu = new action_menu;
1049          $menu->attributes['class'] .= ' view-mode-selector vms ml-1';
1050  
1051          $selected = null;
1052          foreach ($modes as $mode => $modestr) {
1053              $attributes = array(
1054                  'class' => 'vms-mode',
1055                  'data-mode' => $mode
1056              );
1057              if ($currentmode === $mode) {
1058                  $attributes['class'] .= ' currentmode';
1059                  $selected = $modestr;
1060              }
1061              if ($selected === null) {
1062                  $selected = $modestr;
1063              }
1064              $modeurl = new moodle_url($url, array($param => $mode));
1065              if ($mode === 'default') {
1066                  $modeurl->remove_params($param);
1067              }
1068              $menu->add(new action_menu_link_secondary($modeurl, null, $modestr, $attributes));
1069          }
1070  
1071          $menu->set_menu_trigger($selected);
1072  
1073          $html = html_writer::start_div('view-mode-selector vms d-flex');
1074          $html .= get_string('viewing').' '.$this->render($menu);
1075          $html .= html_writer::end_div();
1076  
1077          return $html;
1078      }
1079  
1080      /**
1081       * Displays a search result listing.
1082       *
1083       * @param array $courses The courses to display.
1084       * @param int $totalcourses The total number of courses to display.
1085       * @param core_course_list_element $course The currently selected course if there is one.
1086       * @param int $page The current page, starting at 0.
1087       * @param int $perpage The number of courses to display per page.
1088       * @param string $search The string we are searching for.
1089       * @return string
1090       */
1091      public function search_listing(array $courses, $totalcourses, core_course_list_element $course = null, $page = 0, $perpage = 20,
1092              $search = '') {
1093          $page = max($page, 0);
1094          $perpage = max($perpage, 2);
1095          $totalpages = ceil($totalcourses / $perpage);
1096          if ($page > $totalpages - 1) {
1097              $page = $totalpages - 1;
1098          }
1099          $courseid = isset($course) ? $course->id : null;
1100          $first = true;
1101          $last = false;
1102          $i = $page * $perpage;
1103  
1104          $html  = html_writer::start_div('course-listing w-100', array(
1105                  'data-category' => 'search',
1106                  'data-page' => $page,
1107                  'data-totalpages' => $totalpages,
1108                  'data-totalcourses' => $totalcourses
1109          ));
1110          $html .= html_writer::tag('h3', get_string('courses'));
1111          $html .= $this->search_pagination($totalcourses, $page, $perpage);
1112          $html .= html_writer::start_tag('ul', array('class' => 'ml'));
1113          foreach ($courses as $listitem) {
1114              $i++;
1115              if ($i == $totalcourses) {
1116                  $last = true;
1117              }
1118              $html .= $this->search_listitem($listitem, $courseid, $first, $last);
1119              $first = false;
1120          }
1121          $html .= html_writer::end_tag('ul');
1122          $html .= $this->search_pagination($totalcourses, $page, $perpage, true, $search);
1123          $html .= $this->course_search_bulk_actions();
1124          $html .= html_writer::end_div();
1125          return $html;
1126      }
1127  
1128      /**
1129       * Displays pagination for search results.
1130       *
1131       * @param int $totalcourses The total number of courses to be displayed.
1132       * @param int $page The current page.
1133       * @param int $perpage The number of courses being displayed.
1134       * @param bool $showtotals Whether or not to print total information.
1135       * @param string $search The string we are searching for.
1136       * @return string
1137       */
1138      protected function search_pagination($totalcourses, $page, $perpage, $showtotals = false, $search = '') {
1139          $html = '';
1140          $totalpages = ceil($totalcourses / $perpage);
1141          if ($showtotals) {
1142              if ($totalpages == 0) {
1143                  $str = get_string('nocoursesfound', 'moodle', s($search));
1144              } else if ($totalpages == 1) {
1145                  $str = get_string('showingacourses', 'moodle', $totalcourses);
1146              } else {
1147                  $a = new stdClass;
1148                  $a->start = ($page * $perpage) + 1;
1149                  $a->end = min((($page + 1) * $perpage), $totalcourses);
1150                  $a->total = $totalcourses;
1151                  $str = get_string('showingxofycourses', 'moodle', $a);
1152              }
1153              $html .= html_writer::div($str, 'listing-pagination-totals text-muted');
1154          }
1155  
1156          if ($totalcourses < $perpage) {
1157              return $html;
1158          }
1159          $aside = 2;
1160          $span = $aside * 2 + 1;
1161          $start = max($page - $aside, 0);
1162          $end = min($page + $aside, $totalpages - 1);
1163          if (($end - $start) < $span) {
1164              if ($start == 0) {
1165                  $end = min($totalpages - 1, $span - 1);
1166              } else if ($end == ($totalpages - 1)) {
1167                  $start = max(0, $end - $span + 1);
1168              }
1169          }
1170          $items = array();
1171          $baseurl = $this->page->url;
1172          if ($page > 0) {
1173              $items[] = $this->action_button(new moodle_url($baseurl, array('page' => 0)), get_string('first'));
1174              $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $page - 1)), get_string('prev'));
1175              $items[] = '...';
1176          }
1177          for ($i = $start; $i <= $end; $i++) {
1178              $class = '';
1179              if ($page == $i) {
1180                  $class = 'active-page';
1181              }
1182              $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $i)), $i + 1, null, $class);
1183          }
1184          if ($page < ($totalpages - 1)) {
1185              $items[] = '...';
1186              $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $page + 1)), get_string('next'));
1187              $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $totalpages - 1)), get_string('last'));
1188          }
1189  
1190          $html .= html_writer::div(join('', $items), 'listing-pagination');
1191          return $html;
1192      }
1193  
1194      /**
1195       * Renderers a search result course list item.
1196       *
1197       * This function will be called for every course being displayed by course_listing.
1198       *
1199       * @param core_course_list_element $course The course to produce HTML for.
1200       * @param int $selectedcourse The id of the currently selected course.
1201       * @return string
1202       */
1203      public function search_listitem(core_course_list_element $course, $selectedcourse) {
1204  
1205          $text = $course->get_formatted_name();
1206          $attributes = array(
1207                  'class' => 'listitem listitem-course list-group-item list-group-item-action',
1208                  'data-id' => $course->id,
1209                  'data-selected' => ($selectedcourse == $course->id) ? '1' : '0',
1210                  'data-visible' => $course->visible ? '1' : '0'
1211          );
1212          $bulkcourseinput = '';
1213          if (core_course_category::get($course->category)->can_move_courses_out_of()) {
1214              $bulkcourseinput = array(
1215                      'type' => 'checkbox',
1216                      'id' => 'coursesearchlistitem' . $course->id,
1217                      'name' => 'bc[]',
1218                      'value' => $course->id,
1219                      'class' => 'bulk-action-checkbox custom-control-input',
1220                      'data-action' => 'select'
1221              );
1222          }
1223          $viewcourseurl = new moodle_url($this->page->url, array('courseid' => $course->id));
1224          $categoryname = core_course_category::get($course->category)->get_formatted_name();
1225  
1226          $html  = html_writer::start_tag('li', $attributes);
1227          $html .= html_writer::start_div('clearfix');
1228          $html .= html_writer::start_div('float-left');
1229          if ($bulkcourseinput) {
1230              $html .= html_writer::start_div('custom-control custom-checkbox mr-1');
1231              $html .= html_writer::empty_tag('input', $bulkcourseinput);
1232              $labeltext = html_writer::span(get_string('bulkactionselect', 'moodle', $text), 'sr-only');
1233              $html .= html_writer::tag('label', $labeltext, array(
1234                  'class' => 'custom-control-label',
1235                  'for' => 'coursesearchlistitem' . $course->id));
1236              $html .= html_writer::end_div();
1237          }
1238          $html .= html_writer::end_div();
1239          $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename aalink'));
1240          $html .= html_writer::tag('span', $categoryname, array('class' => 'float-left ml-3 text-muted'));
1241          $html .= html_writer::start_div('float-right');
1242          $html .= $this->search_listitem_actions($course);
1243          $html .= html_writer::tag('span', s($course->idnumber), array('class' => 'text-muted idnumber'));
1244          $html .= html_writer::end_div();
1245          $html .= html_writer::end_div();
1246          $html .= html_writer::end_tag('li');
1247          return $html;
1248      }
1249  
1250      /**
1251       * Renderers actions for individual course actions.
1252       *
1253       * @param core_course_list_element  $course The course to renderer actions for.
1254       * @return string
1255       */
1256      public function search_listitem_actions(core_course_list_element $course) {
1257          $baseurl = new moodle_url(
1258              '/course/managementsearch.php',
1259              array('courseid' => $course->id, 'categoryid' => $course->category, 'sesskey' => sesskey())
1260          );
1261          $actions = array();
1262          // Edit.
1263          if ($course->can_access()) {
1264              if ($course->can_edit()) {
1265                  $actions[] = $this->output->action_icon(
1266                      new moodle_url('/course/edit.php', array('id' => $course->id)),
1267                      new pix_icon('t/edit', get_string('edit')),
1268                      null,
1269                      array('class' => 'action-edit')
1270                  );
1271              }
1272              // Delete.
1273              if ($course->can_delete()) {
1274                  $actions[] = $this->output->action_icon(
1275                      new moodle_url('/course/delete.php', array('id' => $course->id)),
1276                      new pix_icon('t/delete', get_string('delete')),
1277                      null,
1278                      array('class' => 'action-delete')
1279                  );
1280              }
1281              // Show/Hide.
1282              if ($course->can_change_visibility()) {
1283                      $actions[] = $this->output->action_icon(
1284                          new moodle_url($baseurl, array('action' => 'hidecourse')),
1285                          new pix_icon('t/hide', get_string('hide')),
1286                          null,
1287                          array('data-action' => 'hide', 'class' => 'action-hide')
1288                      );
1289                      $actions[] = $this->output->action_icon(
1290                          new moodle_url($baseurl, array('action' => 'showcourse')),
1291                          new pix_icon('t/show', get_string('show')),
1292                          null,
1293                          array('data-action' => 'show', 'class' => 'action-show')
1294                      );
1295              }
1296          }
1297          if (empty($actions)) {
1298              return '';
1299          }
1300          return html_writer::span(join('', $actions), 'course-item-actions item-actions');
1301      }
1302  
1303      /**
1304       * Renders html to display a course search form
1305       *
1306       * @deprecated since Moodle 4.0. This is now handled within manage_categories_action_bar
1307       * @todo Final deprecation MDL-73975
1308       * @param string $value default value to populate the search field
1309       * @return string
1310       */
1311      public function course_search_form($value = '') {
1312          debugging('course_search_form() is deprecated. Use the class manage_categories_action_bar instead.', DEBUG_DEVELOPER);
1313          $data = [
1314              'action' => new moodle_url('/course/management.php'),
1315              'btnclass' => 'btn-primary',
1316              'extraclasses' => 'my-3 d-flex justify-content-center',
1317              'inputname' => 'search',
1318              'searchstring' => get_string('searchcourses'),
1319              'value' => $value
1320          ];
1321          return $this->render_from_template('core/search_input', $data);
1322      }
1323  
1324      /**
1325       * Creates access hidden skip to links for the displayed sections.
1326       *
1327       * @param bool $displaycategorylisting
1328       * @param bool $displaycourselisting
1329       * @param bool $displaycoursedetail
1330       * @return string
1331       */
1332      public function accessible_skipto_links($displaycategorylisting, $displaycourselisting, $displaycoursedetail) {
1333          $html = html_writer::start_div('skiplinks accesshide');
1334          $url = new moodle_url($this->page->url);
1335          if ($displaycategorylisting) {
1336              $url->set_anchor('category-listing');
1337              $html .= html_writer::link($url, get_string('skiptocategorylisting'), array('class' => 'skip'));
1338          }
1339          if ($displaycourselisting) {
1340              $url->set_anchor('course-listing');
1341              $html .= html_writer::link($url, get_string('skiptocourselisting'), array('class' => 'skip'));
1342          }
1343          if ($displaycoursedetail) {
1344              $url->set_anchor('course-detail');
1345              $html .= html_writer::link($url, get_string('skiptocoursedetails'), array('class' => 'skip'));
1346          }
1347          $html .= html_writer::end_div();
1348          return $html;
1349      }
1350  
1351      /**
1352       * Render the tertiary nav for the manage categories page.
1353       *
1354       * @param \core_course\output\manage_categories_action_bar $actionbar
1355       * @return string The renderered template
1356       */
1357      public function render_action_bar(\core_course\output\manage_categories_action_bar $actionbar): string {
1358          return $this->render_from_template('core_course/manage_category_actionbar', $actionbar->export_for_template($this));
1359      }
1360  }