on-demand release 3.8dev+
[moodle.git] / course / classes / management_renderer.php
blob1f12b7f7146da36b6a4490a8c20252d1d62f84eb
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 $html .= $this->render($select);
90 $html .= html_writer::end_div();
92 $html .= html_writer::end_div();
93 return $html;
96 /**
97 * Prepares the form element for the course category listing bulk actions.
99 * @return string
101 public function management_form_start() {
102 $form = array('action' => $this->page->url->out(), 'method' => 'POST', 'id' => 'coursecat-management');
104 $html = html_writer::start_tag('form', $form);
105 $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
106 $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'action', 'value' => 'bulkaction'));
107 return $html;
111 * Closes the course category bulk management form.
113 * @return string
115 public function management_form_end() {
116 return html_writer::end_tag('form');
120 * Presents a course category listing.
122 * @param core_course_category $category The currently selected category. Also the category to highlight in the listing.
123 * @return string
125 public function category_listing(core_course_category $category = null) {
127 if ($category === null) {
128 $selectedparents = array();
129 $selectedcategory = null;
130 } else {
131 $selectedparents = $category->get_parents();
132 $selectedparents[] = $category->id;
133 $selectedcategory = $category->id;
135 $catatlevel = \core_course\management\helper::get_expanded_categories('');
136 $catatlevel[] = array_shift($selectedparents);
137 $catatlevel = array_unique($catatlevel);
139 $listing = core_course_category::top()->get_children();
141 $attributes = array(
142 'class' => 'ml-1 list-unstyled',
143 'role' => 'tree',
144 'aria-labelledby' => 'category-listing-title'
147 $html = html_writer::start_div('category-listing card w-100');
148 $html .= html_writer::tag('h3', get_string('categories'),
149 array('class' => 'card-header', 'id' => 'category-listing-title'));
150 $html .= html_writer::start_div('card-body');
151 $html .= $this->category_listing_actions($category);
152 $html .= html_writer::start_tag('ul', $attributes);
153 foreach ($listing as $listitem) {
154 // Render each category in the listing.
155 $subcategories = array();
156 if (in_array($listitem->id, $catatlevel)) {
157 $subcategories = $listitem->get_children();
159 $html .= $this->category_listitem(
160 $listitem,
161 $subcategories,
162 $listitem->get_children_count(),
163 $selectedcategory,
164 $selectedparents
167 $html .= html_writer::end_tag('ul');
168 $html .= $this->category_bulk_actions($category);
169 $html .= html_writer::end_div();
170 $html .= html_writer::end_div();
171 return $html;
175 * Renders a category list item.
177 * This function gets called recursively to render sub categories.
179 * @param core_course_category $category The category to render as listitem.
180 * @param core_course_category[] $subcategories The subcategories belonging to the category being rented.
181 * @param int $totalsubcategories The total number of sub categories.
182 * @param int $selectedcategory The currently selected category
183 * @param int[] $selectedcategories The path to the selected category and its ID.
184 * @return string
186 public function category_listitem(core_course_category $category, array $subcategories, $totalsubcategories,
187 $selectedcategory = null, $selectedcategories = array()) {
189 $isexpandable = ($totalsubcategories > 0);
190 $isexpanded = (!empty($subcategories));
191 $activecategory = ($selectedcategory === $category->id);
192 $attributes = array(
193 'class' => 'listitem listitem-category list-group-item list-group-item-action',
194 'data-id' => $category->id,
195 'data-expandable' => $isexpandable ? '1' : '0',
196 'data-expanded' => $isexpanded ? '1' : '0',
197 'data-selected' => $activecategory ? '1' : '0',
198 'data-visible' => $category->visible ? '1' : '0',
199 'role' => 'treeitem',
200 'aria-expanded' => $isexpanded ? 'true' : 'false'
202 $text = $category->get_formatted_name();
203 if (($parent = $category->get_parent_coursecat()) && $parent->id) {
204 $a = new stdClass;
205 $a->category = $text;
206 $a->parentcategory = $parent->get_formatted_name();
207 $textlabel = get_string('categorysubcategoryof', 'moodle', $a);
209 $courseicon = $this->output->pix_icon('i/course', get_string('courses'));
210 $bcatinput = array(
211 'type' => 'checkbox',
212 'name' => 'bcat[]',
213 'value' => $category->id,
214 'class' => 'bulk-action-checkbox',
215 'aria-label' => get_string('bulkactionselect', 'moodle', $text),
216 'data-action' => 'select'
219 if (!$category->can_resort_subcategories() && !$category->has_manage_capability()) {
220 // Very very hardcoded here.
221 $bcatinput['style'] = 'visibility:hidden';
224 $viewcaturl = new moodle_url('/course/management.php', array('categoryid' => $category->id));
225 if ($isexpanded) {
226 $icon = $this->output->pix_icon('t/switch_minus', get_string('collapse'),
227 'moodle', array('class' => 'tree-icon', 'title' => ''));
228 $icon = html_writer::link(
229 $viewcaturl,
230 $icon,
231 array(
232 'class' => 'float-left',
233 'data-action' => 'collapse',
234 'title' => get_string('collapsecategory', 'moodle', $text),
235 'aria-controls' => 'subcategoryof'.$category->id
238 } else if ($isexpandable) {
239 $icon = $this->output->pix_icon('t/switch_plus', get_string('expand'),
240 'moodle', array('class' => 'tree-icon', 'title' => ''));
241 $icon = html_writer::link(
242 $viewcaturl,
243 $icon,
244 array(
245 'class' => 'float-left',
246 'data-action' => 'expand',
247 'title' => get_string('expandcategory', 'moodle', $text)
250 } else {
251 $icon = $this->output->pix_icon(
252 'i/empty',
254 'moodle',
255 array('class' => 'tree-icon'));
256 $icon = html_writer::span($icon, 'float-left');
258 $actions = \core_course\management\helper::get_category_listitem_actions($category);
259 $hasactions = !empty($actions) || $category->can_create_course();
261 $html = html_writer::start_tag('li', $attributes);
262 $html .= html_writer::start_div('clearfix');
263 $html .= html_writer::start_div('float-left ba-checkbox');
264 $html .= html_writer::empty_tag('input', $bcatinput).'&nbsp;';
265 $html .= html_writer::end_div();
266 $html .= $icon;
267 if ($hasactions) {
268 $textattributes = array('class' => 'float-left categoryname');
269 } else {
270 $textattributes = array('class' => 'float-left categoryname without-actions');
272 if (isset($textlabel)) {
273 $textattributes['aria-label'] = $textlabel;
275 $html .= html_writer::link($viewcaturl, $text, $textattributes);
276 $html .= html_writer::start_div('float-right d-flex');
277 if ($category->idnumber) {
278 $html .= html_writer::tag('span', s($category->idnumber), array('class' => 'dimmed idnumber'));
280 if ($hasactions) {
281 $html .= $this->category_listitem_actions($category, $actions);
283 $countid = 'course-count-'.$category->id;
284 $html .= html_writer::span(
285 html_writer::span($category->get_courses_count()) .
286 html_writer::span(get_string('courses'), 'accesshide', array('id' => $countid)) .
287 $courseicon,
288 'course-count dimmed',
289 array('aria-labelledby' => $countid)
291 $html .= html_writer::end_div();
292 $html .= html_writer::end_div();
293 if ($isexpanded) {
294 $html .= html_writer::start_tag('ul',
295 array('class' => 'ml', 'role' => 'group', 'id' => 'subcategoryof'.$category->id));
296 $catatlevel = \core_course\management\helper::get_expanded_categories($category->path);
297 $catatlevel[] = array_shift($selectedcategories);
298 $catatlevel = array_unique($catatlevel);
299 foreach ($subcategories as $listitem) {
300 $childcategories = (in_array($listitem->id, $catatlevel)) ? $listitem->get_children() : array();
301 $html .= $this->category_listitem(
302 $listitem,
303 $childcategories,
304 $listitem->get_children_count(),
305 $selectedcategory,
306 $selectedcategories
309 $html .= html_writer::end_tag('ul');
311 $html .= html_writer::end_tag('li');
312 return $html;
316 * Renderers the actions that are possible for the course category listing.
318 * These are not the actions associated with an individual category listing.
319 * That happens through category_listitem_actions.
321 * @param core_course_category $category
322 * @return string
324 public function category_listing_actions(core_course_category $category = null) {
325 $actions = array();
327 $cancreatecategory = $category && $category->can_create_subcategory();
328 $cancreatecategory = $cancreatecategory || core_course_category::can_create_top_level_category();
329 if ($category === null) {
330 $category = core_course_category::top();
333 if ($cancreatecategory) {
334 $url = new moodle_url('/course/editcategory.php', array('parent' => $category->id));
335 $actions[] = html_writer::link($url, get_string('createnewcategory'), array('class' => 'btn btn-secondary'));
337 if (core_course_category::can_approve_course_requests()) {
338 $actions[] = html_writer::link(new moodle_url('/course/pending.php'), get_string('coursespending'));
340 if (count($actions) === 0) {
341 return '';
343 return html_writer::div(join(' ', $actions), 'listing-actions category-listing-actions mb-3');
347 * Renderers the actions for individual category list items.
349 * @param core_course_category $category
350 * @param array $actions
351 * @return string
353 public function category_listitem_actions(core_course_category $category, array $actions = null) {
354 if ($actions === null) {
355 $actions = \core_course\management\helper::get_category_listitem_actions($category);
357 $menu = new action_menu();
358 $menu->attributes['class'] .= ' category-item-actions item-actions';
359 $hasitems = false;
360 foreach ($actions as $key => $action) {
361 $hasitems = true;
362 $menu->add(new action_menu_link(
363 $action['url'],
364 $action['icon'],
365 $action['string'],
366 in_array($key, array('show', 'hide', 'moveup', 'movedown')),
367 array('data-action' => $key, 'class' => 'action-'.$key)
370 if (!$hasitems) {
371 return '';
373 return $this->render($menu);
376 public function render_action_menu($menu) {
377 global $OUTPUT;
379 return $OUTPUT->render($menu);
383 * Renders bulk actions for categories.
385 * @param core_course_category $category The currently selected category if there is one.
386 * @return string
388 public function category_bulk_actions(core_course_category $category = null) {
389 // Resort courses.
390 // Change parent.
391 if (!core_course_category::can_resort_any() && !core_course_category::can_change_parent_any()) {
392 return '';
394 $strgo = new lang_string('go');
396 $html = html_writer::start_div('category-bulk-actions bulk-actions');
397 $html .= html_writer::div(get_string('categorybulkaction'), 'accesshide', array('tabindex' => '0'));
398 if (core_course_category::can_resort_any()) {
399 $selectoptions = array(
400 'selectedcategories' => get_string('selectedcategories'),
401 'allcategories' => get_string('allcategories')
403 $form = html_writer::start_div();
404 if ($category) {
405 $selectoptions = array('thiscategory' => get_string('thiscategory')) + $selectoptions;
406 $form .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'currentcategoryid', 'value' => $category->id));
408 $form .= html_writer::div(
409 html_writer::select(
410 $selectoptions,
411 'selectsortby',
412 'selectedcategories',
413 false,
414 array('aria-label' => get_string('selectcategorysort'))
417 $form .= html_writer::div(
418 html_writer::select(
419 array(
420 'name' => get_string('sortbyx', 'moodle', get_string('categoryname')),
421 'namedesc' => get_string('sortbyxreverse', 'moodle', get_string('categoryname')),
422 'idnumber' => get_string('sortbyx', 'moodle', get_string('idnumbercoursecategory')),
423 'idnumberdesc' => get_string('sortbyxreverse' , 'moodle' , get_string('idnumbercoursecategory')),
424 'none' => get_string('dontsortcategories')
426 'resortcategoriesby',
427 'name',
428 false,
429 array('aria-label' => get_string('selectcategorysortby'), 'class' => 'mt-1')
432 $form .= html_writer::div(
433 html_writer::select(
434 array(
435 'fullname' => get_string('sortbyx', 'moodle', get_string('fullnamecourse')),
436 'fullnamedesc' => get_string('sortbyxreverse', 'moodle', get_string('fullnamecourse')),
437 'shortname' => get_string('sortbyx', 'moodle', get_string('shortnamecourse')),
438 'shortnamedesc' => get_string('sortbyxreverse', 'moodle', get_string('shortnamecourse')),
439 'idnumber' => get_string('sortbyx', 'moodle', get_string('idnumbercourse')),
440 'idnumberdesc' => get_string('sortbyxreverse', 'moodle', get_string('idnumbercourse')),
441 'timecreated' => get_string('sortbyx', 'moodle', get_string('timecreatedcourse')),
442 'timecreateddesc' => get_string('sortbyxreverse', 'moodle', get_string('timecreatedcourse')),
443 'none' => get_string('dontsortcourses')
445 'resortcoursesby',
446 'fullname',
447 false,
448 array('aria-label' => get_string('selectcoursesortby'), 'class' => 'mt-1')
451 $form .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'bulksort',
452 'value' => get_string('sort'), 'class' => 'btn btn-secondary my-1'));
453 $form .= html_writer::end_div();
455 $html .= html_writer::start_div('detail-pair row yui3-g my-1');
456 $html .= html_writer::div(html_writer::span(get_string('sorting')), 'pair-key col-md-3 yui3-u-1-4');
457 $html .= html_writer::div($form, 'pair-value col-md-9 yui3-u-3-4');
458 $html .= html_writer::end_div();
460 if (core_course_category::can_change_parent_any()) {
461 $options = array();
462 if (core_course_category::top()->has_manage_capability()) {
463 $options[0] = core_course_category::top()->get_formatted_name();
465 $options += core_course_category::make_categories_list('moodle/category:manage');
466 $select = html_writer::select(
467 $options,
468 'movecategoriesto',
470 array('' => 'choosedots'),
471 array('aria-labelledby' => 'moveselectedcategoriesto', 'class' => 'mr-1')
473 $submit = array('type' => 'submit', 'name' => 'bulkmovecategories', 'value' => get_string('move'),
474 'class' => 'btn btn-secondary');
475 $html .= $this->detail_pair(
476 html_writer::span(get_string('moveselectedcategoriesto'), '', array('id' => 'moveselectedcategoriesto')),
477 $select . html_writer::empty_tag('input', $submit)
480 $html .= html_writer::end_div();
481 return $html;
485 * Renders a course listing.
487 * @param core_course_category $category The currently selected category. This is what the listing is focused on.
488 * @param core_course_list_element $course The currently selected course.
489 * @param int $page The page being displayed.
490 * @param int $perpage The number of courses to display per page.
491 * @param string|null $viewmode The view mode the page is in, one out of 'default', 'combined', 'courses' or 'categories'.
492 * @return string
494 public function course_listing(core_course_category $category = null, core_course_list_element $course = null,
495 $page = 0, $perpage = 20, $viewmode = 'default') {
497 if ($category === null) {
498 $html = html_writer::start_div('select-a-category');
499 $html .= html_writer::tag('h3', get_string('courses'),
500 array('id' => 'course-listing-title', 'tabindex' => '0'));
501 $html .= $this->output->notification(get_string('selectacategory'), 'notifymessage');
502 $html .= html_writer::end_div();
503 return $html;
506 $page = max($page, 0);
507 $perpage = max($perpage, 2);
508 $totalcourses = $category->coursecount;
509 $totalpages = ceil($totalcourses / $perpage);
510 if ($page > $totalpages - 1) {
511 $page = $totalpages - 1;
513 $options = array(
514 'offset' => $page * $perpage,
515 'limit' => $perpage
517 $courseid = isset($course) ? $course->id : null;
518 $class = '';
519 if ($page === 0) {
520 $class .= ' firstpage';
522 if ($page + 1 === (int)$totalpages) {
523 $class .= ' lastpage';
526 $html = html_writer::start_div('card course-listing w-100'.$class, array(
527 'data-category' => $category->id,
528 'data-page' => $page,
529 'data-totalpages' => $totalpages,
530 'data-totalcourses' => $totalcourses,
531 'data-canmoveoutof' => $category->can_move_courses_out_of() && $category->can_move_courses_into()
533 $html .= html_writer::tag('h3', $category->get_formatted_name(),
534 array('id' => 'course-listing-title', 'tabindex' => '0', 'class' => 'card-header'));
535 $html .= html_writer::start_div('card-body');
536 $html .= $this->course_listing_actions($category, $course, $perpage);
537 $html .= $this->listing_pagination($category, $page, $perpage, false, $viewmode);
538 $html .= html_writer::start_tag('ul', array('class' => 'ml course-list', 'role' => 'group'));
539 foreach ($category->get_courses($options) as $listitem) {
540 $html .= $this->course_listitem($category, $listitem, $courseid);
542 $html .= html_writer::end_tag('ul');
543 $html .= $this->listing_pagination($category, $page, $perpage, true, $viewmode);
544 $html .= $this->course_bulk_actions($category);
545 $html .= html_writer::end_div();
546 $html .= html_writer::end_div();
547 return $html;
551 * Renders pagination for a course listing.
553 * @param core_course_category $category The category to produce pagination for.
554 * @param int $page The current page.
555 * @param int $perpage The number of courses to display per page.
556 * @param bool $showtotals Set to true to show the total number of courses and what is being displayed.
557 * @param string|null $viewmode The view mode the page is in, one out of 'default', 'combined', 'courses' or 'categories'.
558 * @return string
560 protected function listing_pagination(core_course_category $category, $page, $perpage, $showtotals = false,
561 $viewmode = 'default') {
562 $html = '';
563 $totalcourses = $category->get_courses_count();
564 $totalpages = ceil($totalcourses / $perpage);
565 if ($showtotals) {
566 if ($totalpages == 0) {
567 $str = get_string('nocoursesyet');
568 } else if ($totalpages == 1) {
569 $str = get_string('showingacourses', 'moodle', $totalcourses);
570 } else {
571 $a = new stdClass;
572 $a->start = ($page * $perpage) + 1;
573 $a->end = min((($page + 1) * $perpage), $totalcourses);
574 $a->total = $totalcourses;
575 $str = get_string('showingxofycourses', 'moodle', $a);
577 $html .= html_writer::div($str, 'listing-pagination-totals dimmed');
580 if ($viewmode !== 'default') {
581 $baseurl = new moodle_url('/course/management.php', array('categoryid' => $category->id,
582 'view' => $viewmode));
583 } else {
584 $baseurl = new moodle_url('/course/management.php', array('categoryid' => $category->id));
587 $html .= $this->output->paging_bar($totalcourses, $page, $perpage, $baseurl);
588 return $html;
592 * Renderers a course list item.
594 * This function will be called for every course being displayed by course_listing.
596 * @param core_course_category $category The currently selected category and the category the course belongs to.
597 * @param core_course_list_element $course The course to produce HTML for.
598 * @param int $selectedcourse The id of the currently selected course.
599 * @return string
601 public function course_listitem(core_course_category $category, core_course_list_element $course, $selectedcourse) {
603 $text = $course->get_formatted_name();
604 $attributes = array(
605 'class' => 'listitem listitem-course list-group-item list-group-item-action',
606 'data-id' => $course->id,
607 'data-selected' => ($selectedcourse == $course->id) ? '1' : '0',
608 'data-visible' => $course->visible ? '1' : '0'
611 $bulkcourseinput = array(
612 'type' => 'checkbox',
613 'name' => 'bc[]',
614 'value' => $course->id,
615 'class' => 'bulk-action-checkbox',
616 'aria-label' => get_string('bulkactionselect', 'moodle', $text),
617 'data-action' => 'select'
619 if (!$category->has_manage_capability()) {
620 // Very very hardcoded here.
621 $bulkcourseinput['style'] = 'visibility:hidden';
624 $viewcourseurl = new moodle_url($this->page->url, array('courseid' => $course->id));
626 $html = html_writer::start_tag('li', $attributes);
627 $html .= html_writer::start_div('clearfix');
629 if ($category->can_resort_courses()) {
630 // In order for dnd to be available the user must be able to resort the category children..
631 $html .= html_writer::div($this->output->pix_icon('i/move_2d', get_string('dndcourse')), 'float-left drag-handle');
634 $html .= html_writer::start_div('ba-checkbox float-left');
635 $html .= html_writer::empty_tag('input', $bulkcourseinput).'&nbsp;';
636 $html .= html_writer::end_div();
637 $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename'));
638 $html .= html_writer::start_div('float-right');
639 if ($course->idnumber) {
640 $html .= html_writer::tag('span', s($course->idnumber), array('class' => 'dimmed idnumber'));
642 $html .= $this->course_listitem_actions($category, $course);
643 $html .= html_writer::end_div();
644 $html .= html_writer::end_div();
645 $html .= html_writer::end_tag('li');
646 return $html;
650 * Renderers actions for the course listing.
652 * Not to be confused with course_listitem_actions which renderers the actions for individual courses.
654 * @param core_course_category $category
655 * @param core_course_list_element $course The currently selected course.
656 * @param int $perpage
657 * @return string
659 public function course_listing_actions(core_course_category $category, core_course_list_element $course = null, $perpage = 20) {
660 $actions = array();
661 if ($category->can_create_course()) {
662 $url = new moodle_url('/course/edit.php', array('category' => $category->id, 'returnto' => 'catmanage'));
663 $actions[] = html_writer::link($url, get_string('createnewcourse'), array('class' => 'btn btn-secondary'));
665 if ($category->can_request_course()) {
666 // Request a new course.
667 $url = new moodle_url('/course/request.php', array('return' => 'management'));
668 $actions[] = html_writer::link($url, get_string('requestcourse'));
670 if ($category->can_resort_courses()) {
671 $params = $this->page->url->params();
672 $params['action'] = 'resortcourses';
673 $params['sesskey'] = sesskey();
674 $baseurl = new moodle_url('/course/management.php', $params);
675 $fullnameurl = new moodle_url($baseurl, array('resort' => 'fullname'));
676 $fullnameurldesc = new moodle_url($baseurl, array('resort' => 'fullnamedesc'));
677 $shortnameurl = new moodle_url($baseurl, array('resort' => 'shortname'));
678 $shortnameurldesc = new moodle_url($baseurl, array('resort' => 'shortnamedesc'));
679 $idnumberurl = new moodle_url($baseurl, array('resort' => 'idnumber'));
680 $idnumberdescurl = new moodle_url($baseurl, array('resort' => 'idnumberdesc'));
681 $timecreatedurl = new moodle_url($baseurl, array('resort' => 'timecreated'));
682 $timecreateddescurl = new moodle_url($baseurl, array('resort' => 'timecreateddesc'));
683 $menu = new action_menu(array(
684 new action_menu_link_secondary($fullnameurl,
685 null,
686 get_string('sortbyx', 'moodle', get_string('fullnamecourse'))),
687 new action_menu_link_secondary($fullnameurldesc,
688 null,
689 get_string('sortbyxreverse', 'moodle', get_string('fullnamecourse'))),
690 new action_menu_link_secondary($shortnameurl,
691 null,
692 get_string('sortbyx', 'moodle', get_string('shortnamecourse'))),
693 new action_menu_link_secondary($shortnameurldesc,
694 null,
695 get_string('sortbyxreverse', 'moodle', get_string('shortnamecourse'))),
696 new action_menu_link_secondary($idnumberurl,
697 null,
698 get_string('sortbyx', 'moodle', get_string('idnumbercourse'))),
699 new action_menu_link_secondary($idnumberdescurl,
700 null,
701 get_string('sortbyxreverse', 'moodle', get_string('idnumbercourse'))),
702 new action_menu_link_secondary($timecreatedurl,
703 null,
704 get_string('sortbyx', 'moodle', get_string('timecreatedcourse'))),
705 new action_menu_link_secondary($timecreateddescurl,
706 null,
707 get_string('sortbyxreverse', 'moodle', get_string('timecreatedcourse')))
709 $menu->set_menu_trigger(get_string('resortcourses'));
710 $actions[] = $this->render($menu);
712 $strall = get_string('all');
713 $menu = new action_menu(array(
714 new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 5)), null, 5),
715 new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 10)), null, 10),
716 new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 20)), null, 20),
717 new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 50)), null, 50),
718 new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 100)), null, 100),
719 new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 999)), null, $strall),
721 if ((int)$perpage === 999) {
722 $perpage = $strall;
724 $menu->attributes['class'] .= ' courses-per-page';
725 $menu->set_menu_trigger(get_string('perpagea', 'moodle', $perpage));
726 $actions[] = $this->render($menu);
727 return html_writer::div(join(' ', $actions), 'listing-actions course-listing-actions');
731 * Renderers actions for individual course actions.
733 * @param core_course_category $category The currently selected category.
734 * @param core_course_list_element $course The course to renderer actions for.
735 * @return string
737 public function course_listitem_actions(core_course_category $category, core_course_list_element $course) {
738 $actions = \core_course\management\helper::get_course_listitem_actions($category, $course);
739 if (empty($actions)) {
740 return '';
742 $actionshtml = array();
743 foreach ($actions as $action) {
744 $action['attributes']['role'] = 'button';
745 $actionshtml[] = $this->output->action_icon($action['url'], $action['icon'], null, $action['attributes']);
747 return html_writer::span(join('', $actionshtml), 'course-item-actions item-actions');
751 * Renderers bulk actions that can be performed on courses.
753 * @param core_course_category $category The currently selected category and the category in which courses that
754 * are selectable belong.
755 * @return string
757 public function course_bulk_actions(core_course_category $category) {
758 $html = html_writer::start_div('course-bulk-actions bulk-actions');
759 if ($category->can_move_courses_out_of()) {
760 $html .= html_writer::div(get_string('coursebulkaction'), 'accesshide', array('tabindex' => '0'));
761 $options = core_course_category::make_categories_list('moodle/category:manage');
762 $select = html_writer::select(
763 $options,
764 'movecoursesto',
766 array('' => 'choosedots'),
767 array('aria-labelledby' => 'moveselectedcoursesto', 'class' => 'mr-1')
769 $submit = array('type' => 'submit', 'name' => 'bulkmovecourses', 'value' => get_string('move'),
770 'class' => 'btn btn-secondary');
771 $html .= $this->detail_pair(
772 html_writer::span(get_string('moveselectedcoursesto'), '', array('id' => 'moveselectedcoursesto')),
773 $select . html_writer::empty_tag('input', $submit)
776 $html .= html_writer::end_div();
777 return $html;
781 * Renderers bulk actions that can be performed on courses in search returns
783 * @return string
785 public function course_search_bulk_actions() {
786 $html = html_writer::start_div('course-bulk-actions bulk-actions');
787 $html .= html_writer::div(get_string('coursebulkaction'), 'accesshide', array('tabindex' => '0'));
788 $options = core_course_category::make_categories_list('moodle/category:manage');
789 $select = html_writer::select(
790 $options,
791 'movecoursesto',
793 array('' => 'choosedots'),
794 array('aria-labelledby' => 'moveselectedcoursesto')
796 $submit = array('type' => 'submit', 'name' => 'bulkmovecourses', 'value' => get_string('move'),
797 'class' => 'btn btn-secondary');
798 $html .= $this->detail_pair(
799 html_writer::span(get_string('moveselectedcoursesto'), '', array('id' => 'moveselectedcoursesto')),
800 $select . html_writer::empty_tag('input', $submit)
802 $html .= html_writer::end_div();
803 return $html;
807 * Renderers detailed course information.
809 * @param core_course_list_element $course The course to display details for.
810 * @return string
812 public function course_detail(core_course_list_element $course) {
813 $details = \core_course\management\helper::get_course_detail_array($course);
814 $fullname = $details['fullname']['value'];
816 $html = html_writer::start_div('course-detail card');
817 $html .= html_writer::start_div('card-header');
818 $html .= html_writer::tag('h3', $fullname, array('id' => 'course-detail-title',
819 'class' => 'card-title', 'tabindex' => '0'));
820 $html .= html_writer::end_div();
821 $html .= html_writer::start_div('card-body');
822 $html .= $this->course_detail_actions($course);
823 foreach ($details as $class => $data) {
824 $html .= $this->detail_pair($data['key'], $data['value'], $class);
826 $html .= html_writer::end_div();
827 $html .= html_writer::end_div();
828 return $html;
832 * Renderers a key value pair of information for display.
834 * @param string $key
835 * @param string $value
836 * @param string $class
837 * @return string
839 protected function detail_pair($key, $value, $class ='') {
840 $html = html_writer::start_div('detail-pair row yui3-g '.preg_replace('#[^a-zA-Z0-9_\-]#', '-', $class));
841 $html .= html_writer::div(html_writer::span($key), 'pair-key col-md-3 yui3-u-1-4 font-weight-bold');
842 $html .= html_writer::div(html_writer::span($value), 'pair-value col-md-8 yui3-u-3-4');
843 $html .= html_writer::end_div();
844 return $html;
848 * A collection of actions for a course.
850 * @param core_course_list_element $course The course to display actions for.
851 * @return string
853 public function course_detail_actions(core_course_list_element $course) {
854 $actions = \core_course\management\helper::get_course_detail_actions($course);
855 if (empty($actions)) {
856 return '';
858 $options = array();
859 foreach ($actions as $action) {
860 $options[] = $this->action_link($action['url'], $action['string'], null,
861 array('class' => 'btn btn-sm btn-secondary mr-1 mb-3'));
863 return html_writer::div(join('', $options), 'listing-actions course-detail-listing-actions');
867 * Creates an action button (styled link)
869 * @param moodle_url $url The URL to go to when clicked.
870 * @param string $text The text for the button.
871 * @param string $id An id to give the button.
872 * @param string $class A class to give the button.
873 * @param array $attributes Any additional attributes
874 * @return string
876 protected function action_button(moodle_url $url, $text, $id = null, $class = null, $title = null, array $attributes = array()) {
877 if (isset($attributes['class'])) {
878 $attributes['class'] .= ' yui3-button';
879 } else {
880 $attributes['class'] = 'yui3-button';
882 if (!is_null($id)) {
883 $attributes['id'] = $id;
885 if (!is_null($class)) {
886 $attributes['class'] .= ' '.$class;
888 if (is_null($title)) {
889 $title = $text;
891 $attributes['title'] = $title;
892 if (!isset($attributes['role'])) {
893 $attributes['role'] = 'button';
895 return html_writer::link($url, $text, $attributes);
899 * Opens a grid.
901 * Call {@link core_course_management_renderer::grid_column_start()} to create columns.
903 * @param string $id An id to give this grid.
904 * @param string $class A class to give this grid.
905 * @return string
907 public function grid_start($id = null, $class = null) {
908 $gridclass = 'grid-start grid-row-r d-flex flex-wrap row';
909 if (is_null($class)) {
910 $class = $gridclass;
911 } else {
912 $class .= ' ' . $gridclass;
914 $attributes = array();
915 if (!is_null($id)) {
916 $attributes['id'] = $id;
918 return html_writer::start_div($class, $attributes);
922 * Closes the grid.
924 * @return string
926 public function grid_end() {
927 return html_writer::end_div();
931 * Opens a grid column
933 * @param int $size The number of segments this column should span.
934 * @param string $id An id to give the column.
935 * @param string $class A class to give the column.
936 * @return string
938 public function grid_column_start($size, $id = null, $class = null) {
940 if ($id == 'course-detail') {
941 $size = 12;
942 $bootstrapclass = 'col-md-'.$size;
943 } else {
944 $bootstrapclass = 'd-flex flex-wrap px-3 mb-3';
947 $yuigridclass = "col-sm";
949 if (is_null($class)) {
950 $class = $yuigridclass . ' ' . $bootstrapclass;
951 } else {
952 $class .= ' ' . $yuigridclass . ' ' . $bootstrapclass;
954 $attributes = array();
955 if (!is_null($id)) {
956 $attributes['id'] = $id;
958 return html_writer::start_div($class . " grid_column_start", $attributes);
962 * Closes a grid column.
964 * @return string
966 public function grid_column_end() {
967 return html_writer::end_div();
971 * Renders an action_icon.
973 * This function uses the {@link core_renderer::action_link()} method for the
974 * most part. What it does different is prepare the icon as HTML and use it
975 * as the link text.
977 * @param string|moodle_url $url A string URL or moodel_url
978 * @param pix_icon $pixicon
979 * @param component_action $action
980 * @param array $attributes associative array of html link attributes + disabled
981 * @param bool $linktext show title next to image in link
982 * @return string HTML fragment
984 public function action_icon($url, pix_icon $pixicon, component_action $action = null,
985 array $attributes = null, $linktext = false) {
986 if (!($url instanceof moodle_url)) {
987 $url = new moodle_url($url);
989 $attributes = (array)$attributes;
991 if (empty($attributes['class'])) {
992 // Let devs override the class via $attributes.
993 $attributes['class'] = 'action-icon';
996 $icon = $this->render($pixicon);
998 if ($linktext) {
999 $text = $pixicon->attributes['alt'];
1000 } else {
1001 $text = '';
1004 return $this->action_link($url, $icon.$text, $action, $attributes);
1008 * Displays a view mode selector.
1010 * @param array $modes An array of view modes.
1011 * @param string $currentmode The current view mode.
1012 * @param moodle_url $url The URL to use when changing actions. Defaults to the page URL.
1013 * @param string $param The param name.
1014 * @return string
1016 public function view_mode_selector(array $modes, $currentmode, moodle_url $url = null, $param = 'view') {
1017 if ($url === null) {
1018 $url = $this->page->url;
1021 $menu = new action_menu;
1022 $menu->attributes['class'] .= ' view-mode-selector vms ml-1';
1024 $selected = null;
1025 foreach ($modes as $mode => $modestr) {
1026 $attributes = array(
1027 'class' => 'vms-mode',
1028 'data-mode' => $mode
1030 if ($currentmode === $mode) {
1031 $attributes['class'] .= ' currentmode';
1032 $selected = $modestr;
1034 if ($selected === null) {
1035 $selected = $modestr;
1037 $modeurl = new moodle_url($url, array($param => $mode));
1038 if ($mode === 'default') {
1039 $modeurl->remove_params($param);
1041 $menu->add(new action_menu_link_secondary($modeurl, null, $modestr, $attributes));
1044 $menu->set_menu_trigger($selected);
1046 $html = html_writer::start_div('view-mode-selector vms d-flex');
1047 $html .= get_string('viewing').' '.$this->render($menu);
1048 $html .= html_writer::end_div();
1050 return $html;
1054 * Displays a search result listing.
1056 * @param array $courses The courses to display.
1057 * @param int $totalcourses The total number of courses to display.
1058 * @param core_course_list_element $course The currently selected course if there is one.
1059 * @param int $page The current page, starting at 0.
1060 * @param int $perpage The number of courses to display per page.
1061 * @param string $search The string we are searching for.
1062 * @return string
1064 public function search_listing(array $courses, $totalcourses, core_course_list_element $course = null, $page = 0, $perpage = 20,
1065 $search = '') {
1066 $page = max($page, 0);
1067 $perpage = max($perpage, 2);
1068 $totalpages = ceil($totalcourses / $perpage);
1069 if ($page > $totalpages - 1) {
1070 $page = $totalpages - 1;
1072 $courseid = isset($course) ? $course->id : null;
1073 $first = true;
1074 $last = false;
1075 $i = $page * $perpage;
1077 $html = html_writer::start_div('course-listing w-100', array(
1078 'data-category' => 'search',
1079 'data-page' => $page,
1080 'data-totalpages' => $totalpages,
1081 'data-totalcourses' => $totalcourses
1083 $html .= html_writer::tag('h3', get_string('courses'));
1084 $html .= $this->search_pagination($totalcourses, $page, $perpage);
1085 $html .= html_writer::start_tag('ul', array('class' => 'ml'));
1086 foreach ($courses as $listitem) {
1087 $i++;
1088 if ($i == $totalcourses) {
1089 $last = true;
1091 $html .= $this->search_listitem($listitem, $courseid, $first, $last);
1092 $first = false;
1094 $html .= html_writer::end_tag('ul');
1095 $html .= $this->search_pagination($totalcourses, $page, $perpage, true, $search);
1096 $html .= $this->course_search_bulk_actions();
1097 $html .= html_writer::end_div();
1098 return $html;
1102 * Displays pagination for search results.
1104 * @param int $totalcourses The total number of courses to be displayed.
1105 * @param int $page The current page.
1106 * @param int $perpage The number of courses being displayed.
1107 * @param bool $showtotals Whether or not to print total information.
1108 * @param string $search The string we are searching for.
1109 * @return string
1111 protected function search_pagination($totalcourses, $page, $perpage, $showtotals = false, $search = '') {
1112 $html = '';
1113 $totalpages = ceil($totalcourses / $perpage);
1114 if ($showtotals) {
1115 if ($totalpages == 0) {
1116 $str = get_string('nocoursesfound', 'moodle', s($search));
1117 } else if ($totalpages == 1) {
1118 $str = get_string('showingacourses', 'moodle', $totalcourses);
1119 } else {
1120 $a = new stdClass;
1121 $a->start = ($page * $perpage) + 1;
1122 $a->end = min((($page + 1) * $perpage), $totalcourses);
1123 $a->total = $totalcourses;
1124 $str = get_string('showingxofycourses', 'moodle', $a);
1126 $html .= html_writer::div($str, 'listing-pagination-totals dimmed');
1129 if ($totalcourses < $perpage) {
1130 return $html;
1132 $aside = 2;
1133 $span = $aside * 2 + 1;
1134 $start = max($page - $aside, 0);
1135 $end = min($page + $aside, $totalpages - 1);
1136 if (($end - $start) < $span) {
1137 if ($start == 0) {
1138 $end = min($totalpages - 1, $span - 1);
1139 } else if ($end == ($totalpages - 1)) {
1140 $start = max(0, $end - $span + 1);
1143 $items = array();
1144 $baseurl = $this->page->url;
1145 if ($page > 0) {
1146 $items[] = $this->action_button(new moodle_url($baseurl, array('page' => 0)), get_string('first'));
1147 $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $page - 1)), get_string('prev'));
1148 $items[] = '...';
1150 for ($i = $start; $i <= $end; $i++) {
1151 $class = '';
1152 if ($page == $i) {
1153 $class = 'active-page';
1155 $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $i)), $i + 1, null, $class);
1157 if ($page < ($totalpages - 1)) {
1158 $items[] = '...';
1159 $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $page + 1)), get_string('next'));
1160 $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $totalpages - 1)), get_string('last'));
1163 $html .= html_writer::div(join('', $items), 'listing-pagination');
1164 return $html;
1168 * Renderers a search result course list item.
1170 * This function will be called for every course being displayed by course_listing.
1172 * @param core_course_list_element $course The course to produce HTML for.
1173 * @param int $selectedcourse The id of the currently selected course.
1174 * @return string
1176 public function search_listitem(core_course_list_element $course, $selectedcourse) {
1178 $text = $course->get_formatted_name();
1179 $attributes = array(
1180 'class' => 'listitem listitem-course list-group-item list-group-item-action',
1181 'data-id' => $course->id,
1182 'data-selected' => ($selectedcourse == $course->id) ? '1' : '0',
1183 'data-visible' => $course->visible ? '1' : '0'
1185 $bulkcourseinput = '';
1186 if (core_course_category::get($course->category)->can_move_courses_out_of()) {
1187 $bulkcourseinput = array(
1188 'type' => 'checkbox',
1189 'name' => 'bc[]',
1190 'value' => $course->id,
1191 'class' => 'bulk-action-checkbox',
1192 'aria-label' => get_string('bulkactionselect', 'moodle', $text),
1193 'data-action' => 'select'
1196 $viewcourseurl = new moodle_url($this->page->url, array('courseid' => $course->id));
1197 $categoryname = core_course_category::get($course->category)->get_formatted_name();
1199 $html = html_writer::start_tag('li', $attributes);
1200 $html .= html_writer::start_div('clearfix');
1201 $html .= html_writer::start_div('float-left');
1202 if ($bulkcourseinput) {
1203 $html .= html_writer::empty_tag('input', $bulkcourseinput).'&nbsp;';
1205 $html .= html_writer::end_div();
1206 $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename'));
1207 $html .= html_writer::tag('span', $categoryname, array('class' => 'float-left categoryname'));
1208 $html .= html_writer::start_div('float-right');
1209 $html .= $this->search_listitem_actions($course);
1210 $html .= html_writer::tag('span', s($course->idnumber), array('class' => 'dimmed idnumber'));
1211 $html .= html_writer::end_div();
1212 $html .= html_writer::end_div();
1213 $html .= html_writer::end_tag('li');
1214 return $html;
1218 * Renderers actions for individual course actions.
1220 * @param core_course_list_element $course The course to renderer actions for.
1221 * @return string
1223 public function search_listitem_actions(core_course_list_element $course) {
1224 $baseurl = new moodle_url(
1225 '/course/managementsearch.php',
1226 array('courseid' => $course->id, 'categoryid' => $course->category, 'sesskey' => sesskey())
1228 $actions = array();
1229 // Edit.
1230 if ($course->can_access()) {
1231 if ($course->can_edit()) {
1232 $actions[] = $this->output->action_icon(
1233 new moodle_url('/course/edit.php', array('id' => $course->id)),
1234 new pix_icon('t/edit', get_string('edit')),
1235 null,
1236 array('class' => 'action-edit')
1239 // Delete.
1240 if ($course->can_delete()) {
1241 $actions[] = $this->output->action_icon(
1242 new moodle_url('/course/delete.php', array('id' => $course->id)),
1243 new pix_icon('t/delete', get_string('delete')),
1244 null,
1245 array('class' => 'action-delete')
1248 // Show/Hide.
1249 if ($course->can_change_visibility()) {
1250 $actions[] = $this->output->action_icon(
1251 new moodle_url($baseurl, array('action' => 'hidecourse')),
1252 new pix_icon('t/hide', get_string('hide')),
1253 null,
1254 array('data-action' => 'hide', 'class' => 'action-hide')
1256 $actions[] = $this->output->action_icon(
1257 new moodle_url($baseurl, array('action' => 'showcourse')),
1258 new pix_icon('t/show', get_string('show')),
1259 null,
1260 array('data-action' => 'show', 'class' => 'action-show')
1264 if (empty($actions)) {
1265 return '';
1267 return html_writer::span(join('', $actions), 'course-item-actions item-actions');
1271 * Renders html to display a course search form
1273 * @param string $value default value to populate the search field
1274 * @param string $format display format - 'plain' (default), 'short' or 'navbar'
1275 * @return string
1277 public function course_search_form($value = '', $format = 'plain') {
1278 static $count = 0;
1279 $formid = 'coursesearch';
1280 if ((++$count) > 1) {
1281 $formid .= $count;
1284 switch ($format) {
1285 case 'navbar' :
1286 $formid = 'coursesearchnavbar';
1287 $inputid = 'navsearchbox';
1288 $inputsize = 20;
1289 break;
1290 case 'short' :
1291 $inputid = 'shortsearchbox';
1292 $inputsize = 12;
1293 break;
1294 default :
1295 $inputid = 'coursesearchbox';
1296 $inputsize = 30;
1299 $strsearchcourses = get_string("searchcourses");
1300 $searchurl = new moodle_url('/course/management.php');
1302 $output = html_writer::start_div('row');
1303 $output .= html_writer::start_div('col-md-12');
1304 $output .= html_writer::start_tag('form', array('class' => 'card', 'id' => $formid,
1305 'action' => $searchurl, 'method' => 'get'));
1306 $output .= html_writer::start_tag('fieldset', array('class' => 'coursesearchbox invisiblefieldset'));
1307 $output .= html_writer::tag('div', $this->output->heading($strsearchcourses.': ', 2, 'm-0'),
1308 array('class' => 'card-header'));
1309 $output .= html_writer::start_div('card-body');
1310 $output .= html_writer::start_div('input-group col-sm-6 col-lg-4 m-auto');
1311 $output .= html_writer::empty_tag('input', array('class' => 'form-control', 'type' => 'text', 'id' => $inputid,
1312 'size' => $inputsize, 'name' => 'search', 'value' => s($value)));
1313 $output .= html_writer::start_tag('span', array('class' => 'input-group-btn'));
1314 $output .= html_writer::tag('button', get_string('go'), array('class' => 'btn btn-primary', 'type' => 'submit'));
1315 $output .= html_writer::end_tag('span');
1316 $output .= html_writer::end_div();
1317 $output .= html_writer::end_div();
1318 $output .= html_writer::end_tag('fieldset');
1319 $output .= html_writer::end_tag('form');
1320 $output .= html_writer::end_div();
1321 $output .= html_writer::end_div();
1323 return $output;
1327 * Creates access hidden skip to links for the displayed sections.
1329 * @param bool $displaycategorylisting
1330 * @param bool $displaycourselisting
1331 * @param bool $displaycoursedetail
1332 * @return string
1334 public function accessible_skipto_links($displaycategorylisting, $displaycourselisting, $displaycoursedetail) {
1335 $html = html_writer::start_div('skiplinks accesshide');
1336 $url = new moodle_url($this->page->url);
1337 if ($displaycategorylisting) {
1338 $url->set_anchor('category-listing');
1339 $html .= html_writer::link($url, get_string('skiptocategorylisting'), array('class' => 'skip'));
1341 if ($displaycourselisting) {
1342 $url->set_anchor('course-listing');
1343 $html .= html_writer::link($url, get_string('skiptocourselisting'), array('class' => 'skip'));
1345 if ($displaycoursedetail) {
1346 $url->set_anchor('course-detail');
1347 $html .= html_writer::link($url, get_string('skiptocoursedetails'), array('class' => 'skip'));
1349 $html .= html_writer::end_div();
1350 return $html;