Merge branch 'MDL-75910-311' of https://github.com/andrewnicols/moodle into MOODLE_31...
[moodle.git] / course / classes / management_renderer.php
blob08373bc922099567dbb557e06ea8d86fbcea0ff7
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.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * Contains renderers for the course management pages.
20 * @package core_course
21 * @copyright 2013 Sam Hemelryk
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 defined('MOODLE_INTERNAL') || die;
27 require_once($CFG->dirroot.'/course/renderer.php');
29 /**
30 * Main renderer for the course management pages.
32 * @package core_course
33 * @copyright 2013 Sam Hemelryk
34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36 class core_course_management_renderer extends plugin_renderer_base {
38 /**
39 * Initialises the JS required to enhance the management interface.
41 * Thunderbirds are go, this function kicks into gear the JS that makes the
42 * course management pages that much cooler.
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'
60 'moodle'
64 /**
65 * Displays a heading for the management pages.
67 * @param string $heading The heading to display
68 * @param string|null $viewmode The current view mode if there are options.
69 * @param int|null $categoryid The currently selected category if there is one.
70 * @return string
72 public function management_heading($heading, $viewmode = null, $categoryid = null) {
73 $html = html_writer::start_div('coursecat-management-header clearfix');
74 if (!empty($heading)) {
75 $html .= $this->heading($heading);
77 if ($viewmode !== null) {
78 $html .= html_writer::start_div();
79 $html .= $this->view_mode_selector(\core_course\management\helper::get_management_viewmodes(), $viewmode);
80 if ($viewmode === 'courses') {
81 $categories = core_course_category::make_categories_list(array('moodle/category:manage', 'moodle/course:create'));
82 $nothing = false;
83 if ($categoryid === null) {
84 $nothing = array('' => get_string('selectacategory'));
85 $categoryid = '';
87 $select = new single_select($this->page->url, 'categoryid', $categories, $categoryid, $nothing);
88 $select->attributes['aria-label'] = get_string('selectacategory');
89 $html .= $this->render($select);
91 $html .= html_writer::end_div();
93 $html .= html_writer::end_div();
94 return $html;
97 /**
98 * Prepares the form element for the course category listing bulk actions.
100 * @return string
102 public function management_form_start() {
103 $form = array('action' => $this->page->url->out(), 'method' => 'POST', 'id' => 'coursecat-management');
105 $html = html_writer::start_tag('form', $form);
106 $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
107 $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'action', 'value' => 'bulkaction'));
108 return $html;
112 * Closes the course category bulk management form.
114 * @return string
116 public function management_form_end() {
117 return html_writer::end_tag('form');
121 * Presents a course category listing.
123 * @param core_course_category $category The currently selected category. Also the category to highlight in the listing.
124 * @return string
126 public function category_listing(core_course_category $category = null) {
128 if ($category === null) {
129 $selectedparents = array();
130 $selectedcategory = null;
131 } else {
132 $selectedparents = $category->get_parents();
133 $selectedparents[] = $category->id;
134 $selectedcategory = $category->id;
136 $catatlevel = \core_course\management\helper::get_expanded_categories('');
137 $catatlevel[] = array_shift($selectedparents);
138 $catatlevel = array_unique($catatlevel);
140 $listing = core_course_category::top()->get_children();
142 $attributes = array(
143 'class' => 'ml-1 list-unstyled',
144 'role' => 'tree',
145 'aria-labelledby' => 'category-listing-title'
148 $html = html_writer::start_div('category-listing card w-100');
149 $html .= html_writer::tag('h3', get_string('categories'),
150 array('class' => 'card-header', 'id' => 'category-listing-title'));
151 $html .= html_writer::start_div('card-body');
152 $html .= $this->category_listing_actions($category);
153 $html .= html_writer::start_tag('ul', $attributes);
154 foreach ($listing as $listitem) {
155 // Render each category in the listing.
156 $subcategories = array();
157 if (in_array($listitem->id, $catatlevel)) {
158 $subcategories = $listitem->get_children();
160 $html .= $this->category_listitem(
161 $listitem,
162 $subcategories,
163 $listitem->get_children_count(),
164 $selectedcategory,
165 $selectedparents
168 $html .= html_writer::end_tag('ul');
169 $html .= $this->category_bulk_actions($category);
170 $html .= html_writer::end_div();
171 $html .= html_writer::end_div();
172 return $html;
176 * Renders a category list item.
178 * This function gets called recursively to render sub categories.
180 * @param core_course_category $category The category to render as listitem.
181 * @param core_course_category[] $subcategories The subcategories belonging to the category being rented.
182 * @param int $totalsubcategories The total number of sub categories.
183 * @param int $selectedcategory The currently selected category
184 * @param int[] $selectedcategories The path to the selected category and its ID.
185 * @return string
187 public function category_listitem(core_course_category $category, array $subcategories, $totalsubcategories,
188 $selectedcategory = null, $selectedcategories = array()) {
190 $isexpandable = ($totalsubcategories > 0);
191 $isexpanded = (!empty($subcategories));
192 $activecategory = ($selectedcategory === $category->id);
193 $attributes = array(
194 'class' => 'listitem listitem-category list-group-item list-group-item-action',
195 'data-id' => $category->id,
196 'data-expandable' => $isexpandable ? '1' : '0',
197 'data-expanded' => $isexpanded ? '1' : '0',
198 'data-selected' => $activecategory ? '1' : '0',
199 'data-visible' => $category->visible ? '1' : '0',
200 'role' => 'treeitem',
201 'aria-expanded' => $isexpanded ? 'true' : 'false'
203 $text = $category->get_formatted_name();
204 if (($parent = $category->get_parent_coursecat()) && $parent->id) {
205 $a = new stdClass;
206 $a->category = $text;
207 $a->parentcategory = $parent->get_formatted_name();
208 $textlabel = get_string('categorysubcategoryof', 'moodle', $a);
210 $courseicon = $this->output->pix_icon('i/course', get_string('courses'));
211 $bcatinput = array(
212 'id' => 'categorylistitem' . $category->id,
213 'type' => 'checkbox',
214 'name' => 'bcat[]',
215 'value' => $category->id,
216 'class' => 'bulk-action-checkbox custom-control-input',
217 'data-action' => 'select'
220 $checkboxclass = '';
221 if (!$category->can_resort_subcategories() && !$category->has_manage_capability()) {
222 // Very very hardcoded here.
223 $checkboxclass = 'd-none';
226 $viewcaturl = new moodle_url('/course/management.php', array('categoryid' => $category->id));
227 if ($isexpanded) {
228 $icon = $this->output->pix_icon('t/switch_minus', get_string('collapse'),
229 'moodle', array('class' => 'tree-icon', 'title' => ''));
230 $icon = html_writer::link(
231 $viewcaturl,
232 $icon,
233 array(
234 'class' => 'float-left',
235 'data-action' => 'collapse',
236 'title' => get_string('collapsecategory', 'moodle', $text),
237 'aria-controls' => 'subcategoryof'.$category->id
240 } else if ($isexpandable) {
241 $icon = $this->output->pix_icon('t/switch_plus', get_string('expand'),
242 'moodle', array('class' => 'tree-icon', 'title' => ''));
243 $icon = html_writer::link(
244 $viewcaturl,
245 $icon,
246 array(
247 'class' => 'float-left',
248 'data-action' => 'expand',
249 'title' => get_string('expandcategory', 'moodle', $text)
252 } else {
253 $icon = $this->output->pix_icon(
254 'i/empty',
256 'moodle',
257 array('class' => 'tree-icon'));
258 $icon = html_writer::span($icon, 'float-left');
260 $actions = \core_course\management\helper::get_category_listitem_actions($category);
261 $hasactions = !empty($actions) || $category->can_create_course();
263 $html = html_writer::start_tag('li', $attributes);
264 $html .= html_writer::start_div('clearfix');
265 $html .= html_writer::start_div('float-left ' . $checkboxclass);
266 $html .= html_writer::start_div('custom-control custom-checkbox mr-1 ');
267 $html .= html_writer::empty_tag('input', $bcatinput);
268 $labeltext = html_writer::span(get_string('bulkactionselect', 'moodle', $text), 'sr-only');
269 $html .= html_writer::tag('label', $labeltext, array(
270 'class' => 'custom-control-label',
271 'for' => 'categorylistitem' . $category->id));
272 $html .= html_writer::end_div();
273 $html .= html_writer::end_div();
274 $html .= $icon;
275 if ($hasactions) {
276 $textattributes = array('class' => 'float-left categoryname aalink');
277 } else {
278 $textattributes = array('class' => 'float-left categoryname without-actions');
280 if (isset($textlabel)) {
281 $textattributes['aria-label'] = $textlabel;
283 $html .= html_writer::link($viewcaturl, $text, $textattributes);
284 $html .= html_writer::start_div('float-right d-flex');
285 if ($category->idnumber) {
286 $html .= html_writer::tag('span', s($category->idnumber), array('class' => 'text-muted idnumber'));
288 if ($hasactions) {
289 $html .= $this->category_listitem_actions($category, $actions);
291 $countid = 'course-count-'.$category->id;
292 $html .= html_writer::span(
293 html_writer::span($category->get_courses_count()) .
294 html_writer::span(get_string('courses'), 'accesshide', array('id' => $countid)) .
295 $courseicon,
296 'course-count text-muted',
297 array('aria-labelledby' => $countid)
299 $html .= html_writer::end_div();
300 $html .= html_writer::end_div();
301 if ($isexpanded) {
302 $html .= html_writer::start_tag('ul',
303 array('class' => 'ml', 'role' => 'group', 'id' => 'subcategoryof'.$category->id));
304 $catatlevel = \core_course\management\helper::get_expanded_categories($category->path);
305 $catatlevel[] = array_shift($selectedcategories);
306 $catatlevel = array_unique($catatlevel);
307 foreach ($subcategories as $listitem) {
308 $childcategories = (in_array($listitem->id, $catatlevel)) ? $listitem->get_children() : array();
309 $html .= $this->category_listitem(
310 $listitem,
311 $childcategories,
312 $listitem->get_children_count(),
313 $selectedcategory,
314 $selectedcategories
317 $html .= html_writer::end_tag('ul');
319 $html .= html_writer::end_tag('li');
320 return $html;
324 * Renderers the actions that are possible for the course category listing.
326 * These are not the actions associated with an individual category listing.
327 * That happens through category_listitem_actions.
329 * @param core_course_category $category
330 * @return string
332 public function category_listing_actions(core_course_category $category = null) {
333 $actions = array();
335 $cancreatecategory = $category && $category->can_create_subcategory();
336 $cancreatecategory = $cancreatecategory || core_course_category::can_create_top_level_category();
337 if ($category === null) {
338 $category = core_course_category::top();
341 if ($cancreatecategory) {
342 $url = new moodle_url('/course/editcategory.php', array('parent' => $category->id));
343 $actions[] = html_writer::link($url, get_string('createnewcategory'), array('class' => 'btn btn-secondary'));
345 if (core_course_category::can_approve_course_requests()) {
346 $actions[] = html_writer::link(new moodle_url('/course/pending.php'), get_string('coursespending'));
348 if (count($actions) === 0) {
349 return '';
351 return html_writer::div(join(' ', $actions), 'listing-actions category-listing-actions mb-3');
355 * Renderers the actions for individual category list items.
357 * @param core_course_category $category
358 * @param array $actions
359 * @return string
361 public function category_listitem_actions(core_course_category $category, array $actions = null) {
362 if ($actions === null) {
363 $actions = \core_course\management\helper::get_category_listitem_actions($category);
365 $menu = new action_menu();
366 $menu->attributes['class'] .= ' category-item-actions item-actions';
367 $hasitems = false;
368 foreach ($actions as $key => $action) {
369 $hasitems = true;
370 $menu->add(new action_menu_link(
371 $action['url'],
372 $action['icon'],
373 $action['string'],
374 in_array($key, array('show', 'hide', 'moveup', 'movedown')),
375 array('data-action' => $key, 'class' => 'action-'.$key)
378 if (!$hasitems) {
379 return '';
382 // If the action menu has items, add the menubar role to the main element containing it.
383 $menu->attributes['role'] = 'menubar';
385 return $this->render($menu);
388 public function render_action_menu($menu) {
389 return $this->output->render($menu);
393 * Renders bulk actions for categories.
395 * @param core_course_category $category The currently selected category if there is one.
396 * @return string
398 public function category_bulk_actions(core_course_category $category = null) {
399 // Resort courses.
400 // Change parent.
401 if (!core_course_category::can_resort_any() && !core_course_category::can_change_parent_any()) {
402 return '';
404 $strgo = new lang_string('go');
406 $html = html_writer::start_div('category-bulk-actions bulk-actions');
407 $html .= html_writer::div(get_string('categorybulkaction'), 'accesshide', array('tabindex' => '0'));
408 if (core_course_category::can_resort_any()) {
409 $selectoptions = array(
410 'selectedcategories' => get_string('selectedcategories'),
411 'allcategories' => get_string('allcategories')
413 $form = html_writer::start_div();
414 if ($category) {
415 $selectoptions = array('thiscategory' => get_string('thiscategory')) + $selectoptions;
416 $form .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'currentcategoryid', 'value' => $category->id));
418 $form .= html_writer::div(
419 html_writer::select(
420 $selectoptions,
421 'selectsortby',
422 'selectedcategories',
423 false,
424 array('aria-label' => get_string('selectcategorysort'))
427 $form .= html_writer::div(
428 html_writer::select(
429 array(
430 'name' => get_string('sortbyx', 'moodle', get_string('categoryname')),
431 'namedesc' => get_string('sortbyxreverse', 'moodle', get_string('categoryname')),
432 'idnumber' => get_string('sortbyx', 'moodle', get_string('idnumbercoursecategory')),
433 'idnumberdesc' => get_string('sortbyxreverse' , 'moodle' , get_string('idnumbercoursecategory')),
434 'none' => get_string('dontsortcategories')
436 'resortcategoriesby',
437 'name',
438 false,
439 array('aria-label' => get_string('selectcategorysortby'), 'class' => 'mt-1')
442 $form .= html_writer::div(
443 html_writer::select(
444 array(
445 'fullname' => get_string('sortbyx', 'moodle', get_string('fullnamecourse')),
446 'fullnamedesc' => get_string('sortbyxreverse', 'moodle', get_string('fullnamecourse')),
447 'shortname' => get_string('sortbyx', 'moodle', get_string('shortnamecourse')),
448 'shortnamedesc' => get_string('sortbyxreverse', 'moodle', get_string('shortnamecourse')),
449 'idnumber' => get_string('sortbyx', 'moodle', get_string('idnumbercourse')),
450 'idnumberdesc' => get_string('sortbyxreverse', 'moodle', get_string('idnumbercourse')),
451 'timecreated' => get_string('sortbyx', 'moodle', get_string('timecreatedcourse')),
452 'timecreateddesc' => get_string('sortbyxreverse', 'moodle', get_string('timecreatedcourse')),
453 'none' => get_string('dontsortcourses')
455 'resortcoursesby',
456 'fullname',
457 false,
458 array('aria-label' => get_string('selectcoursesortby'), 'class' => 'mt-1')
461 $form .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'bulksort',
462 'value' => get_string('sort'), 'class' => 'btn btn-secondary my-1'));
463 $form .= html_writer::end_div();
465 $html .= html_writer::start_div('detail-pair row yui3-g my-1');
466 $html .= html_writer::div(html_writer::span(get_string('sorting')), 'pair-key col-md-3 yui3-u-1-4');
467 $html .= html_writer::div($form, 'pair-value col-md-9 yui3-u-3-4');
468 $html .= html_writer::end_div();
470 if (core_course_category::can_change_parent_any()) {
471 $options = array();
472 if (core_course_category::top()->has_manage_capability()) {
473 $options[0] = core_course_category::top()->get_formatted_name();
475 $options += core_course_category::make_categories_list('moodle/category:manage');
476 $select = html_writer::select(
477 $options,
478 'movecategoriesto',
480 array('' => 'choosedots'),
481 array('aria-labelledby' => 'moveselectedcategoriesto', 'class' => 'mr-1')
483 $submit = array('type' => 'submit', 'name' => 'bulkmovecategories', 'value' => get_string('move'),
484 'class' => 'btn btn-secondary');
485 $html .= $this->detail_pair(
486 html_writer::span(get_string('moveselectedcategoriesto'), '', array('id' => 'moveselectedcategoriesto')),
487 $select . html_writer::empty_tag('input', $submit)
490 $html .= html_writer::end_div();
491 return $html;
495 * Renders a course listing.
497 * @param core_course_category $category The currently selected category. This is what the listing is focused on.
498 * @param core_course_list_element $course The currently selected course.
499 * @param int $page The page being displayed.
500 * @param int $perpage The number of courses to display per page.
501 * @param string|null $viewmode The view mode the page is in, one out of 'default', 'combined', 'courses' or 'categories'.
502 * @return string
504 public function course_listing(core_course_category $category = null, core_course_list_element $course = null,
505 $page = 0, $perpage = 20, $viewmode = 'default') {
507 if ($category === null) {
508 $html = html_writer::start_div('select-a-category');
509 $html .= html_writer::tag('h3', get_string('courses'),
510 array('id' => 'course-listing-title', 'tabindex' => '0'));
511 $html .= $this->output->notification(get_string('selectacategory'), 'notifymessage');
512 $html .= html_writer::end_div();
513 return $html;
516 $page = max($page, 0);
517 $perpage = max($perpage, 2);
518 $totalcourses = $category->coursecount;
519 $totalpages = ceil($totalcourses / $perpage);
520 if ($page > $totalpages - 1) {
521 $page = $totalpages - 1;
523 $options = array(
524 'offset' => $page * $perpage,
525 'limit' => $perpage
527 $courseid = isset($course) ? $course->id : null;
528 $class = '';
529 if ($page === 0) {
530 $class .= ' firstpage';
532 if ($page + 1 === (int)$totalpages) {
533 $class .= ' lastpage';
536 $html = html_writer::start_div('card course-listing w-100'.$class, array(
537 'data-category' => $category->id,
538 'data-page' => $page,
539 'data-totalpages' => $totalpages,
540 'data-totalcourses' => $totalcourses,
541 'data-canmoveoutof' => $category->can_move_courses_out_of() && $category->can_move_courses_into()
543 $html .= html_writer::tag('h3', $category->get_formatted_name(),
544 array('id' => 'course-listing-title', 'tabindex' => '0', 'class' => 'card-header'));
545 $html .= html_writer::start_div('card-body');
546 $html .= $this->course_listing_actions($category, $course, $perpage);
547 $html .= $this->listing_pagination($category, $page, $perpage, false, $viewmode);
548 $html .= html_writer::start_tag('ul', array('class' => 'ml course-list'));
549 foreach ($category->get_courses($options) as $listitem) {
550 $html .= $this->course_listitem($category, $listitem, $courseid);
552 $html .= html_writer::end_tag('ul');
553 $html .= $this->listing_pagination($category, $page, $perpage, true, $viewmode);
554 $html .= $this->course_bulk_actions($category);
555 $html .= html_writer::end_div();
556 $html .= html_writer::end_div();
557 return $html;
561 * Renders pagination for a course listing.
563 * @param core_course_category $category The category to produce pagination for.
564 * @param int $page The current page.
565 * @param int $perpage The number of courses to display per page.
566 * @param bool $showtotals Set to true to show the total number of courses and what is being displayed.
567 * @param string|null $viewmode The view mode the page is in, one out of 'default', 'combined', 'courses' or 'categories'.
568 * @return string
570 protected function listing_pagination(core_course_category $category, $page, $perpage, $showtotals = false,
571 $viewmode = 'default') {
572 $html = '';
573 $totalcourses = $category->get_courses_count();
574 $totalpages = ceil($totalcourses / $perpage);
575 if ($showtotals) {
576 if ($totalpages == 0) {
577 $str = get_string('nocoursesyet');
578 } else if ($totalpages == 1) {
579 $str = get_string('showingacourses', 'moodle', $totalcourses);
580 } else {
581 $a = new stdClass;
582 $a->start = ($page * $perpage) + 1;
583 $a->end = min((($page + 1) * $perpage), $totalcourses);
584 $a->total = $totalcourses;
585 $str = get_string('showingxofycourses', 'moodle', $a);
587 $html .= html_writer::div($str, 'listing-pagination-totals text-muted');
590 if ($viewmode !== 'default') {
591 $baseurl = new moodle_url('/course/management.php', array('categoryid' => $category->id,
592 'view' => $viewmode));
593 } else {
594 $baseurl = new moodle_url('/course/management.php', array('categoryid' => $category->id));
597 $html .= $this->output->paging_bar($totalcourses, $page, $perpage, $baseurl);
598 return $html;
602 * Renderers a course list item.
604 * This function will be called for every course being displayed by course_listing.
606 * @param core_course_category $category The currently selected category and the category the course belongs to.
607 * @param core_course_list_element $course The course to produce HTML for.
608 * @param int $selectedcourse The id of the currently selected course.
609 * @return string
611 public function course_listitem(core_course_category $category, core_course_list_element $course, $selectedcourse) {
613 $text = $course->get_formatted_name();
614 $attributes = array(
615 'class' => 'listitem listitem-course list-group-item list-group-item-action',
616 'data-id' => $course->id,
617 'data-selected' => ($selectedcourse == $course->id) ? '1' : '0',
618 'data-visible' => $course->visible ? '1' : '0'
621 $bulkcourseinput = array(
622 'id' => 'courselistitem' . $course->id,
623 'type' => 'checkbox',
624 'name' => 'bc[]',
625 'value' => $course->id,
626 'class' => 'bulk-action-checkbox custom-control-input',
627 'data-action' => 'select'
630 $checkboxclass = '';
631 if (!$category->has_manage_capability()) {
632 // Very very hardcoded here.
633 $checkboxclass = 'd-none';
636 $viewcourseurl = new moodle_url($this->page->url, array('courseid' => $course->id));
638 $html = html_writer::start_tag('li', $attributes);
639 $html .= html_writer::start_div('clearfix');
641 if ($category->can_resort_courses()) {
642 // In order for dnd to be available the user must be able to resort the category children..
643 $html .= html_writer::div($this->output->pix_icon('i/move_2d', get_string('dndcourse')), 'float-left drag-handle');
646 $html .= html_writer::start_div('float-left ' . $checkboxclass);
647 $html .= html_writer::start_div('custom-control custom-checkbox mr-1 ');
648 $html .= html_writer::empty_tag('input', $bulkcourseinput);
649 $labeltext = html_writer::span(get_string('bulkactionselect', 'moodle', $text), 'sr-only');
650 $html .= html_writer::tag('label', $labeltext, array(
651 'class' => 'custom-control-label',
652 'for' => 'courselistitem' . $course->id));
653 $html .= html_writer::end_div();
654 $html .= html_writer::end_div();
655 $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename aalink'));
656 $html .= html_writer::start_div('float-right');
657 if ($course->idnumber) {
658 $html .= html_writer::tag('span', s($course->idnumber), array('class' => 'text-muted idnumber'));
660 $html .= $this->course_listitem_actions($category, $course);
661 $html .= html_writer::end_div();
662 $html .= html_writer::end_div();
663 $html .= html_writer::end_tag('li');
664 return $html;
668 * Renderers actions for the course listing.
670 * Not to be confused with course_listitem_actions which renderers the actions for individual courses.
672 * @param core_course_category $category
673 * @param core_course_list_element $course The currently selected course.
674 * @param int $perpage
675 * @return string
677 public function course_listing_actions(core_course_category $category, core_course_list_element $course = null, $perpage = 20) {
678 $actions = array();
679 if ($category->can_create_course()) {
680 $url = new moodle_url('/course/edit.php', array('category' => $category->id, 'returnto' => 'catmanage'));
681 $actions[] = html_writer::link($url, get_string('createnewcourse'), array('class' => 'btn btn-secondary'));
683 if ($category->can_request_course()) {
684 // Request a new course.
685 $url = new moodle_url('/course/request.php', array('category' => $category->id, 'return' => 'management'));
686 $actions[] = html_writer::link($url, get_string('requestcourse'));
688 if ($category->can_resort_courses()) {
689 $params = $this->page->url->params();
690 $params['action'] = 'resortcourses';
691 $params['sesskey'] = sesskey();
692 $baseurl = new moodle_url('/course/management.php', $params);
693 $fullnameurl = new moodle_url($baseurl, array('resort' => 'fullname'));
694 $fullnameurldesc = new moodle_url($baseurl, array('resort' => 'fullnamedesc'));
695 $shortnameurl = new moodle_url($baseurl, array('resort' => 'shortname'));
696 $shortnameurldesc = new moodle_url($baseurl, array('resort' => 'shortnamedesc'));
697 $idnumberurl = new moodle_url($baseurl, array('resort' => 'idnumber'));
698 $idnumberdescurl = new moodle_url($baseurl, array('resort' => 'idnumberdesc'));
699 $timecreatedurl = new moodle_url($baseurl, array('resort' => 'timecreated'));
700 $timecreateddescurl = new moodle_url($baseurl, array('resort' => 'timecreateddesc'));
701 $menu = new action_menu(array(
702 new action_menu_link_secondary($fullnameurl,
703 null,
704 get_string('sortbyx', 'moodle', get_string('fullnamecourse'))),
705 new action_menu_link_secondary($fullnameurldesc,
706 null,
707 get_string('sortbyxreverse', 'moodle', get_string('fullnamecourse'))),
708 new action_menu_link_secondary($shortnameurl,
709 null,
710 get_string('sortbyx', 'moodle', get_string('shortnamecourse'))),
711 new action_menu_link_secondary($shortnameurldesc,
712 null,
713 get_string('sortbyxreverse', 'moodle', get_string('shortnamecourse'))),
714 new action_menu_link_secondary($idnumberurl,
715 null,
716 get_string('sortbyx', 'moodle', get_string('idnumbercourse'))),
717 new action_menu_link_secondary($idnumberdescurl,
718 null,
719 get_string('sortbyxreverse', 'moodle', get_string('idnumbercourse'))),
720 new action_menu_link_secondary($timecreatedurl,
721 null,
722 get_string('sortbyx', 'moodle', get_string('timecreatedcourse'))),
723 new action_menu_link_secondary($timecreateddescurl,
724 null,
725 get_string('sortbyxreverse', 'moodle', get_string('timecreatedcourse')))
727 $menu->set_menu_trigger(get_string('resortcourses'));
728 $actions[] = $this->render($menu);
730 $strall = get_string('all');
731 $menu = new action_menu(array(
732 new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 5)), null, 5),
733 new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 10)), null, 10),
734 new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 20)), null, 20),
735 new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 50)), null, 50),
736 new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 100)), null, 100),
737 new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 999)), null, $strall),
739 if ((int)$perpage === 999) {
740 $perpage = $strall;
742 $menu->attributes['class'] .= ' courses-per-page';
743 $menu->set_menu_trigger(get_string('perpagea', 'moodle', $perpage));
744 $actions[] = $this->render($menu);
745 return html_writer::div(join(' ', $actions), 'listing-actions course-listing-actions');
749 * Renderers actions for individual course actions.
751 * @param core_course_category $category The currently selected category.
752 * @param core_course_list_element $course The course to renderer actions for.
753 * @return string
755 public function course_listitem_actions(core_course_category $category, core_course_list_element $course) {
756 $actions = \core_course\management\helper::get_course_listitem_actions($category, $course);
757 if (empty($actions)) {
758 return '';
760 $actionshtml = array();
761 foreach ($actions as $action) {
762 $action['attributes']['role'] = 'button';
763 $actionshtml[] = $this->output->action_icon($action['url'], $action['icon'], null, $action['attributes']);
765 return html_writer::span(join('', $actionshtml), 'course-item-actions item-actions');
769 * Renderers bulk actions that can be performed on courses.
771 * @param core_course_category $category The currently selected category and the category in which courses that
772 * are selectable belong.
773 * @return string
775 public function course_bulk_actions(core_course_category $category) {
776 $html = html_writer::start_div('course-bulk-actions bulk-actions');
777 if ($category->can_move_courses_out_of()) {
778 $html .= html_writer::div(get_string('coursebulkaction'), 'accesshide', array('tabindex' => '0'));
779 $options = core_course_category::make_categories_list('moodle/category:manage');
780 $select = html_writer::select(
781 $options,
782 'movecoursesto',
784 array('' => 'choosedots'),
785 array('aria-labelledby' => 'moveselectedcoursesto', 'class' => 'mr-1')
787 $submit = array('type' => 'submit', 'name' => 'bulkmovecourses', 'value' => get_string('move'),
788 'class' => 'btn btn-secondary');
789 $html .= $this->detail_pair(
790 html_writer::span(get_string('moveselectedcoursesto'), '', array('id' => 'moveselectedcoursesto')),
791 $select . html_writer::empty_tag('input', $submit)
794 $html .= html_writer::end_div();
795 return $html;
799 * Renderers bulk actions that can be performed on courses in search returns
801 * @return string
803 public function course_search_bulk_actions() {
804 $html = html_writer::start_div('course-bulk-actions bulk-actions');
805 $html .= html_writer::div(get_string('coursebulkaction'), 'accesshide', array('tabindex' => '0'));
806 $options = core_course_category::make_categories_list('moodle/category:manage');
807 $select = html_writer::select(
808 $options,
809 'movecoursesto',
811 array('' => 'choosedots'),
812 array('aria-labelledby' => 'moveselectedcoursesto')
814 $submit = array('type' => 'submit', 'name' => 'bulkmovecourses', 'value' => get_string('move'),
815 'class' => 'btn btn-secondary');
816 $html .= $this->detail_pair(
817 html_writer::span(get_string('moveselectedcoursesto'), '', array('id' => 'moveselectedcoursesto')),
818 $select . html_writer::empty_tag('input', $submit)
820 $html .= html_writer::end_div();
821 return $html;
825 * Renderers detailed course information.
827 * @param core_course_list_element $course The course to display details for.
828 * @return string
830 public function course_detail(core_course_list_element $course) {
831 $details = \core_course\management\helper::get_course_detail_array($course);
832 $fullname = $details['fullname']['value'];
834 $html = html_writer::start_div('course-detail card');
835 $html .= html_writer::start_div('card-header');
836 $html .= html_writer::tag('h3', $fullname, array('id' => 'course-detail-title',
837 'class' => 'card-title', 'tabindex' => '0'));
838 $html .= html_writer::end_div();
839 $html .= html_writer::start_div('card-body');
840 $html .= $this->course_detail_actions($course);
841 foreach ($details as $class => $data) {
842 $html .= $this->detail_pair($data['key'], $data['value'], $class);
844 $html .= html_writer::end_div();
845 $html .= html_writer::end_div();
846 return $html;
850 * Renderers a key value pair of information for display.
852 * @param string $key
853 * @param string $value
854 * @param string $class
855 * @return string
857 protected function detail_pair($key, $value, $class ='') {
858 $html = html_writer::start_div('detail-pair row yui3-g '.preg_replace('#[^a-zA-Z0-9_\-]#', '-', $class));
859 $html .= html_writer::div(html_writer::span($key), 'pair-key col-md-3 yui3-u-1-4 font-weight-bold');
860 $html .= html_writer::div(html_writer::span($value), 'pair-value col-md-8 yui3-u-3-4');
861 $html .= html_writer::end_div();
862 return $html;
866 * A collection of actions for a course.
868 * @param core_course_list_element $course The course to display actions for.
869 * @return string
871 public function course_detail_actions(core_course_list_element $course) {
872 $actions = \core_course\management\helper::get_course_detail_actions($course);
873 if (empty($actions)) {
874 return '';
876 $options = array();
877 foreach ($actions as $action) {
878 $options[] = $this->action_link($action['url'], $action['string'], null,
879 array('class' => 'btn btn-sm btn-secondary mr-1 mb-3'));
881 return html_writer::div(join('', $options), 'listing-actions course-detail-listing-actions');
885 * Creates an action button (styled link)
887 * @param moodle_url $url The URL to go to when clicked.
888 * @param string $text The text for the button.
889 * @param string $id An id to give the button.
890 * @param string $class A class to give the button.
891 * @param array $attributes Any additional attributes
892 * @return string
894 protected function action_button(moodle_url $url, $text, $id = null, $class = null, $title = null, array $attributes = array()) {
895 if (isset($attributes['class'])) {
896 $attributes['class'] .= ' yui3-button';
897 } else {
898 $attributes['class'] = 'yui3-button';
900 if (!is_null($id)) {
901 $attributes['id'] = $id;
903 if (!is_null($class)) {
904 $attributes['class'] .= ' '.$class;
906 if (is_null($title)) {
907 $title = $text;
909 $attributes['title'] = $title;
910 if (!isset($attributes['role'])) {
911 $attributes['role'] = 'button';
913 return html_writer::link($url, $text, $attributes);
917 * Opens a grid.
919 * Call {@link core_course_management_renderer::grid_column_start()} to create columns.
921 * @param string $id An id to give this grid.
922 * @param string $class A class to give this grid.
923 * @return string
925 public function grid_start($id = null, $class = null) {
926 $gridclass = 'grid-start grid-row-r d-flex flex-wrap row';
927 if (is_null($class)) {
928 $class = $gridclass;
929 } else {
930 $class .= ' ' . $gridclass;
932 $attributes = array();
933 if (!is_null($id)) {
934 $attributes['id'] = $id;
936 return html_writer::start_div($class, $attributes);
940 * Closes the grid.
942 * @return string
944 public function grid_end() {
945 return html_writer::end_div();
949 * Opens a grid column
951 * @param int $size The number of segments this column should span.
952 * @param string $id An id to give the column.
953 * @param string $class A class to give the column.
954 * @return string
956 public function grid_column_start($size, $id = null, $class = null) {
958 if ($id == 'course-detail') {
959 $size = 12;
960 $bootstrapclass = 'col-md-'.$size;
961 } else {
962 $bootstrapclass = 'd-flex flex-wrap px-3 mb-3';
965 $yuigridclass = "col-sm";
967 if (is_null($class)) {
968 $class = $yuigridclass . ' ' . $bootstrapclass;
969 } else {
970 $class .= ' ' . $yuigridclass . ' ' . $bootstrapclass;
972 $attributes = array();
973 if (!is_null($id)) {
974 $attributes['id'] = $id;
976 return html_writer::start_div($class . " grid_column_start", $attributes);
980 * Closes a grid column.
982 * @return string
984 public function grid_column_end() {
985 return html_writer::end_div();
989 * Renders an action_icon.
991 * This function uses the {@link core_renderer::action_link()} method for the
992 * most part. What it does different is prepare the icon as HTML and use it
993 * as the link text.
995 * @param string|moodle_url $url A string URL or moodel_url
996 * @param pix_icon $pixicon
997 * @param component_action $action
998 * @param array $attributes associative array of html link attributes + disabled
999 * @param bool $linktext show title next to image in link
1000 * @return string HTML fragment
1002 public function action_icon($url, pix_icon $pixicon, component_action $action = null,
1003 array $attributes = null, $linktext = false) {
1004 if (!($url instanceof moodle_url)) {
1005 $url = new moodle_url($url);
1007 $attributes = (array)$attributes;
1009 if (empty($attributes['class'])) {
1010 // Let devs override the class via $attributes.
1011 $attributes['class'] = 'action-icon';
1014 $icon = $this->render($pixicon);
1016 if ($linktext) {
1017 $text = $pixicon->attributes['alt'];
1018 } else {
1019 $text = '';
1022 return $this->action_link($url, $icon.$text, $action, $attributes);
1026 * Displays a view mode selector.
1028 * @param array $modes An array of view modes.
1029 * @param string $currentmode The current view mode.
1030 * @param moodle_url $url The URL to use when changing actions. Defaults to the page URL.
1031 * @param string $param The param name.
1032 * @return string
1034 public function view_mode_selector(array $modes, $currentmode, moodle_url $url = null, $param = 'view') {
1035 if ($url === null) {
1036 $url = $this->page->url;
1039 $menu = new action_menu;
1040 $menu->attributes['class'] .= ' view-mode-selector vms ml-1';
1042 $selected = null;
1043 foreach ($modes as $mode => $modestr) {
1044 $attributes = array(
1045 'class' => 'vms-mode',
1046 'data-mode' => $mode
1048 if ($currentmode === $mode) {
1049 $attributes['class'] .= ' currentmode';
1050 $selected = $modestr;
1052 if ($selected === null) {
1053 $selected = $modestr;
1055 $modeurl = new moodle_url($url, array($param => $mode));
1056 if ($mode === 'default') {
1057 $modeurl->remove_params($param);
1059 $menu->add(new action_menu_link_secondary($modeurl, null, $modestr, $attributes));
1062 $menu->set_menu_trigger($selected);
1064 $html = html_writer::start_div('view-mode-selector vms d-flex');
1065 $html .= get_string('viewing').' '.$this->render($menu);
1066 $html .= html_writer::end_div();
1068 return $html;
1072 * Displays a search result listing.
1074 * @param array $courses The courses to display.
1075 * @param int $totalcourses The total number of courses to display.
1076 * @param core_course_list_element $course The currently selected course if there is one.
1077 * @param int $page The current page, starting at 0.
1078 * @param int $perpage The number of courses to display per page.
1079 * @param string $search The string we are searching for.
1080 * @return string
1082 public function search_listing(array $courses, $totalcourses, core_course_list_element $course = null, $page = 0, $perpage = 20,
1083 $search = '') {
1084 $page = max($page, 0);
1085 $perpage = max($perpage, 2);
1086 $totalpages = ceil($totalcourses / $perpage);
1087 if ($page > $totalpages - 1) {
1088 $page = $totalpages - 1;
1090 $courseid = isset($course) ? $course->id : null;
1091 $first = true;
1092 $last = false;
1093 $i = $page * $perpage;
1095 $html = html_writer::start_div('course-listing w-100', array(
1096 'data-category' => 'search',
1097 'data-page' => $page,
1098 'data-totalpages' => $totalpages,
1099 'data-totalcourses' => $totalcourses
1101 $html .= html_writer::tag('h3', get_string('courses'));
1102 $html .= $this->search_pagination($totalcourses, $page, $perpage);
1103 $html .= html_writer::start_tag('ul', array('class' => 'ml'));
1104 foreach ($courses as $listitem) {
1105 $i++;
1106 if ($i == $totalcourses) {
1107 $last = true;
1109 $html .= $this->search_listitem($listitem, $courseid, $first, $last);
1110 $first = false;
1112 $html .= html_writer::end_tag('ul');
1113 $html .= $this->search_pagination($totalcourses, $page, $perpage, true, $search);
1114 $html .= $this->course_search_bulk_actions();
1115 $html .= html_writer::end_div();
1116 return $html;
1120 * Displays pagination for search results.
1122 * @param int $totalcourses The total number of courses to be displayed.
1123 * @param int $page The current page.
1124 * @param int $perpage The number of courses being displayed.
1125 * @param bool $showtotals Whether or not to print total information.
1126 * @param string $search The string we are searching for.
1127 * @return string
1129 protected function search_pagination($totalcourses, $page, $perpage, $showtotals = false, $search = '') {
1130 $html = '';
1131 $totalpages = ceil($totalcourses / $perpage);
1132 if ($showtotals) {
1133 if ($totalpages == 0) {
1134 $str = get_string('nocoursesfound', 'moodle', s($search));
1135 } else if ($totalpages == 1) {
1136 $str = get_string('showingacourses', 'moodle', $totalcourses);
1137 } else {
1138 $a = new stdClass;
1139 $a->start = ($page * $perpage) + 1;
1140 $a->end = min((($page + 1) * $perpage), $totalcourses);
1141 $a->total = $totalcourses;
1142 $str = get_string('showingxofycourses', 'moodle', $a);
1144 $html .= html_writer::div($str, 'listing-pagination-totals text-muted');
1147 if ($totalcourses < $perpage) {
1148 return $html;
1150 $aside = 2;
1151 $span = $aside * 2 + 1;
1152 $start = max($page - $aside, 0);
1153 $end = min($page + $aside, $totalpages - 1);
1154 if (($end - $start) < $span) {
1155 if ($start == 0) {
1156 $end = min($totalpages - 1, $span - 1);
1157 } else if ($end == ($totalpages - 1)) {
1158 $start = max(0, $end - $span + 1);
1161 $items = array();
1162 $baseurl = $this->page->url;
1163 if ($page > 0) {
1164 $items[] = $this->action_button(new moodle_url($baseurl, array('page' => 0)), get_string('first'));
1165 $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $page - 1)), get_string('prev'));
1166 $items[] = '...';
1168 for ($i = $start; $i <= $end; $i++) {
1169 $class = '';
1170 if ($page == $i) {
1171 $class = 'active-page';
1173 $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $i)), $i + 1, null, $class);
1175 if ($page < ($totalpages - 1)) {
1176 $items[] = '...';
1177 $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $page + 1)), get_string('next'));
1178 $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $totalpages - 1)), get_string('last'));
1181 $html .= html_writer::div(join('', $items), 'listing-pagination');
1182 return $html;
1186 * Renderers a search result course list item.
1188 * This function will be called for every course being displayed by course_listing.
1190 * @param core_course_list_element $course The course to produce HTML for.
1191 * @param int $selectedcourse The id of the currently selected course.
1192 * @return string
1194 public function search_listitem(core_course_list_element $course, $selectedcourse) {
1196 $text = $course->get_formatted_name();
1197 $attributes = array(
1198 'class' => 'listitem listitem-course list-group-item list-group-item-action',
1199 'data-id' => $course->id,
1200 'data-selected' => ($selectedcourse == $course->id) ? '1' : '0',
1201 'data-visible' => $course->visible ? '1' : '0'
1203 $bulkcourseinput = '';
1204 if (core_course_category::get($course->category)->can_move_courses_out_of()) {
1205 $bulkcourseinput = array(
1206 'type' => 'checkbox',
1207 'id' => 'coursesearchlistitem' . $course->id,
1208 'name' => 'bc[]',
1209 'value' => $course->id,
1210 'class' => 'bulk-action-checkbox custom-control-input',
1211 'data-action' => 'select'
1214 $viewcourseurl = new moodle_url($this->page->url, array('courseid' => $course->id));
1215 $categoryname = core_course_category::get($course->category)->get_formatted_name();
1217 $html = html_writer::start_tag('li', $attributes);
1218 $html .= html_writer::start_div('clearfix');
1219 $html .= html_writer::start_div('float-left');
1220 if ($bulkcourseinput) {
1221 $html .= html_writer::start_div('custom-control custom-checkbox mr-1');
1222 $html .= html_writer::empty_tag('input', $bulkcourseinput);
1223 $labeltext = html_writer::span(get_string('bulkactionselect', 'moodle', $text), 'sr-only');
1224 $html .= html_writer::tag('label', $labeltext, array(
1225 'class' => 'custom-control-label',
1226 'for' => 'coursesearchlistitem' . $course->id));
1227 $html .= html_writer::end_div();
1229 $html .= html_writer::end_div();
1230 $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename aalink'));
1231 $html .= html_writer::tag('span', $categoryname, array('class' => 'float-left ml-3 text-muted'));
1232 $html .= html_writer::start_div('float-right');
1233 $html .= $this->search_listitem_actions($course);
1234 $html .= html_writer::tag('span', s($course->idnumber), array('class' => 'text-muted idnumber'));
1235 $html .= html_writer::end_div();
1236 $html .= html_writer::end_div();
1237 $html .= html_writer::end_tag('li');
1238 return $html;
1242 * Renderers actions for individual course actions.
1244 * @param core_course_list_element $course The course to renderer actions for.
1245 * @return string
1247 public function search_listitem_actions(core_course_list_element $course) {
1248 $baseurl = new moodle_url(
1249 '/course/managementsearch.php',
1250 array('courseid' => $course->id, 'categoryid' => $course->category, 'sesskey' => sesskey())
1252 $actions = array();
1253 // Edit.
1254 if ($course->can_access()) {
1255 if ($course->can_edit()) {
1256 $actions[] = $this->output->action_icon(
1257 new moodle_url('/course/edit.php', array('id' => $course->id)),
1258 new pix_icon('t/edit', get_string('edit')),
1259 null,
1260 array('class' => 'action-edit')
1263 // Delete.
1264 if ($course->can_delete()) {
1265 $actions[] = $this->output->action_icon(
1266 new moodle_url('/course/delete.php', array('id' => $course->id)),
1267 new pix_icon('t/delete', get_string('delete')),
1268 null,
1269 array('class' => 'action-delete')
1272 // Show/Hide.
1273 if ($course->can_change_visibility()) {
1274 $actions[] = $this->output->action_icon(
1275 new moodle_url($baseurl, array('action' => 'hidecourse')),
1276 new pix_icon('t/hide', get_string('hide')),
1277 null,
1278 array('data-action' => 'hide', 'class' => 'action-hide')
1280 $actions[] = $this->output->action_icon(
1281 new moodle_url($baseurl, array('action' => 'showcourse')),
1282 new pix_icon('t/show', get_string('show')),
1283 null,
1284 array('data-action' => 'show', 'class' => 'action-show')
1288 if (empty($actions)) {
1289 return '';
1291 return html_writer::span(join('', $actions), 'course-item-actions item-actions');
1295 * Renders html to display a course search form
1297 * @param string $value default value to populate the search field
1298 * @return string
1300 public function course_search_form($value = '') {
1302 $data = [
1303 'action' => new moodle_url('/course/management.php'),
1304 'btnclass' => 'btn-primary',
1305 'extraclasses' => 'my-3 d-flex justify-content-center',
1306 'inputname' => 'search',
1307 'searchstring' => get_string('searchcourses'),
1308 'value' => $value
1310 return $this->render_from_template('core/search_input', $data);
1314 * Creates access hidden skip to links for the displayed sections.
1316 * @param bool $displaycategorylisting
1317 * @param bool $displaycourselisting
1318 * @param bool $displaycoursedetail
1319 * @return string
1321 public function accessible_skipto_links($displaycategorylisting, $displaycourselisting, $displaycoursedetail) {
1322 $html = html_writer::start_div('skiplinks accesshide');
1323 $url = new moodle_url($this->page->url);
1324 if ($displaycategorylisting) {
1325 $url->set_anchor('category-listing');
1326 $html .= html_writer::link($url, get_string('skiptocategorylisting'), array('class' => 'skip'));
1328 if ($displaycourselisting) {
1329 $url->set_anchor('course-listing');
1330 $html .= html_writer::link($url, get_string('skiptocourselisting'), array('class' => 'skip'));
1332 if ($displaycoursedetail) {
1333 $url->set_anchor('course-detail');
1334 $html .= html_writer::link($url, get_string('skiptocoursedetails'), array('class' => 'skip'));
1336 $html .= html_writer::end_div();
1337 return $html;