diff --git a/admin/roles/assign.php b/admin/roles/assign.php
index 10be913c2a2..7935781ee8a 100644
--- a/admin/roles/assign.php
+++ b/admin/roles/assign.php
@@ -183,7 +183,10 @@ switch ($context->contextlevel) {
break;
}
-$PAGE->set_navigation_overflow_state(false);
+// In a course category context we leverage overflow for the tertiary navigation
+if ($context->contextlevel != CONTEXT_COURSECAT) {
+ $PAGE->set_navigation_overflow_state(false);
+}
// Within a course context we need to explicitly set active tab as there isn't a reference in the nav tree.
if ($context->contextlevel == CONTEXT_COURSE) {
diff --git a/admin/roles/check.php b/admin/roles/check.php
index c47b8d411ac..15abd5dc455 100644
--- a/admin/roles/check.php
+++ b/admin/roles/check.php
@@ -126,7 +126,11 @@ if (!is_null($reportuser)) {
$rolenames = role_get_names($context);
}
-$PAGE->set_navigation_overflow_state(false);
+// In a course category context we leverage overflow for the tertiary navigation
+if ($context->contextlevel != CONTEXT_COURSECAT) {
+ $PAGE->set_navigation_overflow_state(false);
+}
+
echo $OUTPUT->header();
if ($context->contextlevel == CONTEXT_COURSE || $context->contextlevel == CONTEXT_MODULE) {
echo $OUTPUT->render_participants_tertiary_nav($course);
diff --git a/admin/roles/permissions.php b/admin/roles/permissions.php
index ed9cc9bbec6..301171b545e 100644
--- a/admin/roles/permissions.php
+++ b/admin/roles/permissions.php
@@ -204,7 +204,11 @@ if ($capability && ($allowoverrides || ($allowsafeoverrides && is_safe_capabilit
}
}
-$PAGE->set_navigation_overflow_state(false);
+// When we are within a category context we leave it to the overflow state to display additional nav nodes.
+if ($context->contextlevel != CONTEXT_COURSECAT) {
+ $PAGE->set_navigation_overflow_state(false);
+}
+
echo $OUTPUT->header();
if ($context->contextlevel == CONTEXT_COURSE || $context->contextlevel == CONTEXT_MODULE) {
echo $OUTPUT->render_participants_tertiary_nav($course);
diff --git a/course/classes/category.php b/course/classes/category.php
index 1733d520cf3..0e83c627000 100644
--- a/course/classes/category.php
+++ b/course/classes/category.php
@@ -2777,6 +2777,17 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
return has_capability('moodle/category:manage', $this->get_context());
}
+ /**
+ * Checks whether the category has access to content bank
+ *
+ * @return bool
+ */
+ public function has_contentbank() {
+ $cb = new \core_contentbank\contentbank();
+ return ($cb->is_context_allowed($this->get_context()) &&
+ has_capability('moodle/contentbank:access', $this->get_context()));
+ }
+
/**
* Returns true if the user has the manage capability on the parent category.
* @return bool
diff --git a/course/classes/management/helper.php b/course/classes/management/helper.php
index 80ba0e4f2c9..d428acc12b9 100644
--- a/course/classes/management/helper.php
+++ b/course/classes/management/helper.php
@@ -173,6 +173,14 @@ class helper {
$manageurl = new \moodle_url('/course/management.php', array('categoryid' => $category->id));
$baseurl = new \moodle_url($manageurl, array('sesskey' => \sesskey()));
$actions = array();
+
+ // View link.
+ $actions['view'] = [
+ 'url' => new \moodle_url('/course/index.php', ['categoryid' => $category->id]),
+ 'icon' => null,
+ 'string' => get_string('view')
+ ];
+
// Edit.
if ($category->can_edit()) {
$actions['edit'] = array(
@@ -252,36 +260,15 @@ class helper {
);
}
- // Assign roles.
- if ($category->can_review_roles()) {
- $actions['assignroles'] = array(
- 'url' => new \moodle_url('/admin/roles/assign.php', array('contextid' => $category->get_context()->id,
- 'returnurl' => $manageurl->out_as_local_url(false))),
- 'icon' => new \pix_icon('t/assignroles', new \lang_string('assignroles', 'role')),
- 'string' => new \lang_string('assignroles', 'role')
- );
- }
-
// Permissions.
if ($category->can_review_permissions()) {
$actions['permissions'] = array(
- 'url' => new \moodle_url('/admin/roles/permissions.php', array('contextid' => $category->get_context()->id,
- 'returnurl' => $manageurl->out_as_local_url(false))),
+ 'url' => new \moodle_url('/admin/roles/permissions.php', ['contextid' => $category->get_context()->id]),
'icon' => new \pix_icon('i/permissions', new \lang_string('permissions', 'role')),
'string' => new \lang_string('permissions', 'role')
);
}
- // Check permissions.
- if ($category->can_review_permissions()) {
- $actions['checkroles'] = array(
- 'url' => new \moodle_url('/admin/roles/check.php', array('contextid' => $category->get_context()->id,
- 'returnurl' => $manageurl->out_as_local_url(false))),
- 'icon' => new \pix_icon('i/checkpermissions', new \lang_string('checkpermissions', 'role')),
- 'string' => new \lang_string('checkpermissions', 'role')
- );
- }
-
// Context locking.
if (!empty($CFG->contextlocking) && has_capability('moodle/site:managecontextlocks', $category->get_context())) {
$parentcontext = $category->get_context()->get_parent_context();
@@ -351,6 +338,16 @@ class helper {
}
}
+ // Content bank.
+ if ($category->has_contentbank()) {
+ $url = new \moodle_url('/contentbank/index.php', ['contextid' => $category->get_context()->id]);
+ $actions['contentbank'] = [
+ 'url' => $url,
+ 'icon' => new \pix_icon('i/contentbank', ''),
+ 'string' => get_string('contentbank')
+ ];
+ }
+
return $actions;
}
diff --git a/course/classes/management_renderer.php b/course/classes/management_renderer.php
index 08373bc9220..838e7fcdf2f 100644
--- a/course/classes/management_renderer.php
+++ b/course/classes/management_renderer.php
@@ -64,12 +64,16 @@ class core_course_management_renderer extends plugin_renderer_base {
/**
* Displays a heading for the management pages.
*
+ * @deprecated since Moodle 4.0. This is now handled/replaced with the tertiary navigation
+ * @todo Final deprecation MDL-73975
* @param string $heading The heading to display
* @param string|null $viewmode The current view mode if there are options.
* @param int|null $categoryid The currently selected category if there is one.
* @return string
*/
public function management_heading($heading, $viewmode = null, $categoryid = null) {
+ debugging('management_heading() is deprecated. Use the class manage_categories_action_bar instead.', DEBUG_DEVELOPER);
+
$html = html_writer::start_div('coursecat-management-header clearfix');
if (!empty($heading)) {
$html .= $this->heading($heading);
@@ -1294,11 +1298,13 @@ class core_course_management_renderer extends plugin_renderer_base {
/**
* Renders html to display a course search form
*
+ * @deprecated since Moodle 4.0. This is now handled within manage_categories_action_bar
+ * @todo Final deprecation MDL-73975
* @param string $value default value to populate the search field
* @return string
*/
public function course_search_form($value = '') {
-
+ debugging('course_search_form() is deprecated. Use the class manage_categories_action_bar instead.', DEBUG_DEVELOPER);
$data = [
'action' => new moodle_url('/course/management.php'),
'btnclass' => 'btn-primary',
@@ -1337,4 +1343,13 @@ class core_course_management_renderer extends plugin_renderer_base {
return $html;
}
+ /**
+ * Render the tertiary nav for the manage categories page.
+ *
+ * @param \core_course\output\manage_categories_action_bar $actionbar
+ * @return string The renderered template
+ */
+ public function render_action_bar(\core_course\output\manage_categories_action_bar $actionbar): string {
+ return $this->render_from_template('core_course/manage_category_actionbar', $actionbar->export_for_template($this));
+ }
}
diff --git a/course/classes/output/category_action_bar.php b/course/classes/output/category_action_bar.php
new file mode 100644
index 00000000000..45cb61dd8e0
--- /dev/null
+++ b/course/classes/output/category_action_bar.php
@@ -0,0 +1,171 @@
+.
+
+namespace core_course\output;
+
+use context_coursecat;
+use core_course_category;
+use course_request;
+use moodle_page;
+use moodle_url;
+
+/**
+ * Class responsible for generating the action bar (tertiary nav) elements in an individual category page
+ *
+ * @package core
+ * @copyright 2021 Peter Dias
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class category_action_bar extends manage_categories_action_bar {
+ /**
+ * @var object The current category we are referring to.
+ */
+ protected $category;
+ /**
+ * Constructor category_action_bar
+ *
+ * @param moodle_page $page The page object
+ * @param object $category
+ * @param object|null $course The course that we are generating the nav for
+ * @param string|null $searchvalue
+ */
+ public function __construct(moodle_page $page, object $category, ?object $course = null, ?string $searchvalue = null) {
+ $this->category = $category;
+ parent::__construct($page, 'courses', $course, $searchvalue);
+ }
+
+ /**
+ * Gets the url_select to be displayed in the participants page if available.
+ *
+ * @param \renderer_base $output
+ * @return object|null The content required to render the url_select
+ */
+ protected function get_category_select(\renderer_base $output): ?object {
+ if (!$this->searchvalue && !core_course_category::is_simple_site()) {
+ $categories = core_course_category::make_categories_list();
+ if (count($categories) > 1) {
+ foreach ($categories as $id => $cat) {
+ $url = new moodle_url($this->page->url, ['categoryid' => $id]);
+ $options[$url->out()] = $cat;
+ }
+ $currenturl = new moodle_url($this->page->url, ['categoryid' => $this->category->id]);
+ $select = new \url_select($options, $currenturl, null);
+ $select->set_label(get_string('categories'), ['class' => 'sr-only']);
+ $select->class .= ' text-truncate w-100';
+ return $select->export_for_template($output);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets the additional options to be displayed within a 'More' dropdown in the tertiary navigation.
+ * The predefined order defined by UX is:
+ * - Add a course
+ * - Add a sub cat
+ * - Manage course
+ * - Request a course
+ * - Course pending approval
+ *
+ * @return array
+ */
+ protected function get_additional_category_options(): array {
+ global $CFG, $DB;
+ if ($this->category->is_uservisible()) {
+ $context = get_category_or_system_context($this->category->id);
+ if (has_capability('moodle/course:create', $context)) {
+ $params = [
+ 'category' => $this->category->id ?: $CFG->defaultrequestcategory,
+ 'returnto' => $this->category->id ? 'category' : 'topcat'
+ ];
+
+ $options[0] = [
+ 'url' => new moodle_url('/course/edit.php', $params),
+ 'string' => get_string('addnewcourse')
+ ];
+ }
+
+ if (!empty($CFG->enablecourserequests)) {
+ // Display an option to request a new course.
+ if (course_request::can_request($context)) {
+ $params = [];
+ if ($context instanceof context_coursecat) {
+ $params['category'] = $context->instanceid;
+ }
+
+ $options[3] = [
+ 'url' => new moodle_url('/course/request.php', $params),
+ 'string' => get_string('requestcourse')
+ ];
+ }
+
+ // Display the manage pending requests option.
+ if (has_capability('moodle/site:approvecourse', $context)) {
+ $disabled = !$DB->record_exists('course_request', array());
+ if (!$disabled) {
+ $options[4] = [
+ 'url' => new moodle_url('/course/pending.php'),
+ 'string' => get_string('coursespending')
+ ];
+ }
+ }
+ }
+ }
+
+ if ($this->category->can_create_course() || $this->category->has_manage_capability()) {
+ // Add 'Manage' button if user has permissions to edit this category.
+ $options[2] = [
+ 'url' => new moodle_url('/course/management.php', ['categoryid' => $this->category->id]),
+ 'string' => get_string('managecourses')
+ ];
+
+ if ($this->category->has_manage_capability()) {
+ $addsubcaturl = new moodle_url('/course/editcategory.php', array('parent' => $this->category->id));
+ $options[1] = [
+ 'url' => $addsubcaturl,
+ 'string' => get_string('addsubcategory')
+ ];
+ }
+ }
+
+ // We have stored the options in a predefined order. Sort it based on index and return.
+ if (isset($options)) {
+ sort($options);
+ return ['options' => $options];
+ }
+
+ return [];
+ }
+
+ /**
+ * Export the content to be displayed on the category page.
+ *
+ * @param \renderer_base $output
+ * @return array Consists of the following:
+ * - categoryselect A list of available categories to be fed into a urlselect
+ * - search The course search form
+ * - additionaloptions Additional actions that can be performed in a category
+ */
+ public function export_for_template(\renderer_base $output): array {
+ return [
+ 'categoryselect' => $this->get_category_select($output),
+ 'search' => $this->get_search_form(),
+ 'additionaloptions' => $this->get_additional_category_options()
+ ];
+ }
+
+}
diff --git a/course/classes/output/manage_categories_action_bar.php b/course/classes/output/manage_categories_action_bar.php
new file mode 100644
index 00000000000..70fee710d66
--- /dev/null
+++ b/course/classes/output/manage_categories_action_bar.php
@@ -0,0 +1,156 @@
+.
+
+namespace core_course\output;
+
+use moodle_page;
+use moodle_url;
+
+/**
+ * Class responsible for generating the action bar (tertiary nav) elements in the category management page
+ *
+ * @package core
+ * @copyright 2021 Peter Dias
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class manage_categories_action_bar implements \renderable {
+ /** @var object $course The course we are dealing with. */
+ protected $course;
+ /** @var moodle_page $page The current page. */
+ protected $page;
+ /** @var string|null $viewmode The viewmode of the underlying page - Course and categories, categories or courses */
+ protected $viewmode;
+ /** @var string|null $heading The heading to display */
+ protected $heading;
+ /** @var string|null $searchvalue The search value if any */
+ protected $searchvalue;
+
+ /**
+ * Constructor for the manage_categories_action_bar
+ *
+ * @param moodle_page $page The page object
+ * @param string $viewmode The type of page we are viewing.
+ * @param object|null $course The course that we are generating the nav for
+ * @param string|null $searchvalue The search value if applicable
+ */
+ public function __construct(moodle_page $page, string $viewmode, ?object $course, ?string $searchvalue) {
+ $this->course = $course;
+ $this->page = $page;
+ $this->viewmode = $viewmode;
+ $this->searchvalue = $searchvalue;
+ if ($searchvalue) {
+ $this->heading = get_string('searchresults');
+ }
+ }
+
+ /**
+ * Gets the url_select to be displayed in the participants page if available.
+ *
+ * @param \renderer_base $output
+ * @return object|null The content required to render the url_select
+ */
+ protected function get_dropdown(\renderer_base $output): ?object {
+ // If a search is being performed then no need to display the dropdown.
+ if ($this->searchvalue) {
+ return null;
+ }
+
+ $modes = \core_course\management\helper::get_management_viewmodes();
+ $activeurl = null;
+ $content = [];
+ foreach ($modes as $mode => $description) {
+ $url = new moodle_url($this->page->url, ['view' => $mode]);
+ $content[$url->out()] = $description;
+ if ($this->viewmode == $mode) {
+ $activeurl = $url->out();
+ $this->heading = get_string("manage$mode");
+ }
+ }
+
+ // Default to the first option if asking for default. This is combined.
+ if (!$activeurl && $this->viewmode === 'default') {
+ $activeurl = array_key_first($content);
+ $this->heading = get_string("managecombined");
+ }
+
+ if ($content) {
+ $urlselect = new \url_select($content, $activeurl, null);
+ $urlselect->set_label(get_string('viewing'), ['class' => 'sr-only']);
+ return $urlselect->export_for_template($output);
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets the url_select to be displayed in the participants page if available.
+ *
+ * @param \renderer_base $output
+ * @return object|null The content required to render the url_select
+ */
+ protected function get_category_select(\renderer_base $output): ?object {
+ if (!$this->searchvalue && $this->viewmode === 'courses') {
+ $categories = \core_course_category::make_categories_list(array('moodle/category:manage', 'moodle/course:create'));
+ $currentcat = $this->page->url->param('categoryid');
+ foreach ($categories as $id => $cat) {
+ $url = new moodle_url($this->page->url, ['categoryid' => $id]);
+ if ($id == $currentcat) {
+ $currenturl = $url->out();
+ }
+ $options[$url->out()] = $cat;
+ }
+
+ $select = new \url_select($options, $currenturl, null);
+ $select->set_label(get_string('category'), ['class' => 'sr-only']);
+ $select->class .= ' text-truncate w-100';
+ return $select->export_for_template($output);
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the search box
+ *
+ * @return array
+ */
+ protected function get_search_form(): array {
+ return [
+ 'action' => new moodle_url('/course/management.php'),
+ 'btnclass' => 'btn-primary',
+ 'inputname' => 'search',
+ 'searchstring' => get_string('searchcourses'),
+ 'query' => $this->searchvalue
+ ];
+ }
+
+ /**
+ * Export the content to be displayed on the participants page.
+ *
+ * @param \renderer_base $output
+ * @return array Consists of the following:
+ * - urlselect A stdclass representing the standard navigation options to be fed into a urlselect
+ * - renderedcontent Rendered content to be displayed in line with the tertiary nav
+ */
+ public function export_for_template(\renderer_base $output): array {
+ return [
+ 'urlselect' => $this->get_dropdown($output),
+ 'categoryselect' => $this->get_category_select($output),
+ 'search' => $this->get_search_form(),
+ 'heading' => $this->heading,
+ ];
+ }
+}
diff --git a/course/edit.php b/course/edit.php
index 34710fc9003..e1050f626aa 100644
--- a/course/edit.php
+++ b/course/edit.php
@@ -112,6 +112,11 @@ if ($id) {
$PAGE->set_context($catcontext);
}
+// We are adding a new course and have a category context.
+if (isset($catcontext)) {
+ $PAGE->set_secondary_active_tab('categorymain');
+}
+
// Prepare course and the editor.
$editoroptions = array('maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes'=>$CFG->maxbytes, 'trusttext'=>false, 'noclean'=>true);
$overviewfilesoptions = course_overviewfiles_options($course);
diff --git a/course/lib.php b/course/lib.php
index 374da482b4d..98ad0e61d5b 100644
--- a/course/lib.php
+++ b/course/lib.php
@@ -650,9 +650,13 @@ function get_category_or_system_context($categoryid) {
* Print the buttons relating to course requests.
*
* @param context $context current page context.
+ * @deprecated since Moodle 4.0
+ * @todo Final deprecation MDL-73976
*/
function print_course_request_buttons($context) {
global $CFG, $DB, $OUTPUT;
+ debugging("print_course_request_buttons() is deprecated. " .
+ "This is replaced with the category_action_bar tertiary navigation.", DEBUG_DEVELOPER);
if (empty($CFG->enablecourserequests)) {
return;
}
diff --git a/course/management.php b/course/management.php
index 7bccc1cf7ec..9601ffd1771 100644
--- a/course/management.php
+++ b/course/management.php
@@ -108,7 +108,7 @@ $PAGE->set_pagelayout('admin');
$PAGE->set_title($strmanagement);
$PAGE->set_heading($pageheading);
$PAGE->requires->js_call_amd('core_course/copy_modal', 'init', array($context->id));
-$PAGE->set_secondary_active_tab('managecategory');
+$PAGE->set_secondary_active_tab('categorymain');
// This is a system level page that operates on other contexts.
require_login();
@@ -473,12 +473,8 @@ $displaycourselisting = ($viewmode === 'default' || $viewmode === 'combined' ||
$displaycoursedetail = (isset($courseid));
echo $renderer->header();
-
-if (!$issearching) {
- echo $renderer->management_heading($strmanagement, $viewmode, $categoryid);
-} else {
- echo $renderer->management_heading(new lang_string('searchresults'));
-}
+$actionbar = new \core_course\output\manage_categories_action_bar($PAGE, $viewmode, $course, $search);
+echo $renderer->render_action_bar($actionbar);
if (count($notificationspass) > 0) {
echo $renderer->notification(join('
', $notificationspass), 'notifysuccess');
@@ -489,8 +485,6 @@ if (count($notificationsfail) > 0) {
// Start the management form.
-echo $renderer->course_search_form($search);
-
echo $renderer->management_form_start();
echo $renderer->accessible_skipto_links($displaycategorylisting, $displaycourselisting, $displaycoursedetail);
diff --git a/course/renderer.php b/course/renderer.php
index 2c97561c3d9..e0db4c9534c 100644
--- a/course/renderer.php
+++ b/course/renderer.php
@@ -1667,14 +1667,8 @@ class core_course_renderer extends plugin_renderer_base {
$coursecat = core_course_category::get(is_object($category) ? $category->id : $category);
}
$site = get_site();
- $output = '';
-
- if ($coursecat->can_create_course() || $coursecat->has_manage_capability()) {
- // Add 'Manage' button if user has permissions to edit this category.
- $managebutton = $this->single_button(new moodle_url('/course/management.php',
- array('categoryid' => $coursecat->id)), get_string('managecourses'), 'get');
- $this->page->set_button($managebutton);
- }
+ $actionbar = new \core_course\output\category_action_bar($this->page, $coursecat);
+ $output = $this->render_from_template('core_course/category_actionbar', $actionbar->export_for_template($this));
if (core_course_category::is_simple_site()) {
// There is only one category in the system, do not display link to it.
@@ -1686,17 +1680,6 @@ class core_course_renderer extends plugin_renderer_base {
} else {
$strfulllistofcourses = get_string('fulllistofcourses');
$this->page->set_title("$site->shortname: $strfulllistofcourses");
-
- // Print the category selector
- $categorieslist = core_course_category::make_categories_list();
- if (count($categorieslist) > 1) {
- $output .= html_writer::start_tag('div', array('class' => 'categorypicker'));
- $select = new single_select(new moodle_url('/course/index.php'), 'categoryid',
- core_course_category::make_categories_list(), $coursecat->id, null, 'switchcategory');
- $select->set_label(get_string('categories').':', ['class' => 'sr-only']);
- $output .= $this->render($select);
- $output .= html_writer::end_tag('div'); // .categorypicker
- }
}
// Print current category description
@@ -1741,33 +1724,10 @@ class core_course_renderer extends plugin_renderer_base {
$catdisplayoptions['viewmoreurl'] = new moodle_url($baseurl, array('browse' => 'categories', 'page' => 1));
}
$chelper->set_courses_display_options($coursedisplayoptions)->set_categories_display_options($catdisplayoptions);
- // Add course search form.
- $output .= $this->course_search_form();
// Display course category tree.
$output .= $this->coursecat_tree($chelper, $coursecat);
- // Add action buttons
- $output .= $this->container_start('buttons mt-3');
- if ($coursecat->is_uservisible()) {
- $context = get_category_or_system_context($coursecat->id);
- if (has_capability('moodle/course:create', $context)) {
- // Print link to create a new course, for the 1st available category.
- if ($coursecat->id) {
- $url = new moodle_url('/course/edit.php', array('category' => $coursecat->id, 'returnto' => 'category'));
- } else {
- $url = new moodle_url('/course/edit.php',
- array('category' => $CFG->defaultrequestcategory, 'returnto' => 'topcat'));
- }
- $output .= $this->single_button($url, get_string('addnewcourse'), 'get');
- }
- ob_start();
- print_course_request_buttons($context);
- $output .= ob_get_contents();
- ob_end_clean();
- }
- $output .= $this->container_end();
-
return $output;
}
diff --git a/course/templates/category_actionbar.mustache b/course/templates/category_actionbar.mustache
new file mode 100644
index 00000000000..f232dae39d3
--- /dev/null
+++ b/course/templates/category_actionbar.mustache
@@ -0,0 +1,117 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see