diff --git a/CHANGES.md b/CHANGES.md
index 08c1c47250f..c761915a3a9 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -6,6 +6,7 @@ Changes
### Unreleased
+* 2023-12-11 - Improvement: Configurable sort order in menu items of smart menu, solves #403.
* 2023-12-10 - Feature: Allow the admin to hide the manual login form and the IDP login intro, solves #490.
* 2023-12-10 - Improvement: Allow the admin to change the look of the course overview block, solves #204
diff --git a/classes/form/smartmenu_item_edit_form.php b/classes/form/smartmenu_item_edit_form.php
index 988b52c2b03..17505d91570 100644
--- a/classes/form/smartmenu_item_edit_form.php
+++ b/classes/form/smartmenu_item_edit_form.php
@@ -248,6 +248,33 @@ public function definition() {
$mform->setType('cssclass', PARAM_TEXT);
$mform->addHelpButton('cssclass', 'smartmenusmenuitemcssclass', 'theme_boost_union');
+ // Add course list ordering (for the dynamic courses menu item type) as select element.
+ $listsortoptions = [
+ smartmenu_item::LISTSORT_FULLNAME_ASC =>
+ get_string('smartmenusmenuitemlistsortfullnameasc', 'theme_boost_union'),
+ smartmenu_item::LISTSORT_FULLNAME_DESC =>
+ get_string('smartmenusmenuitemlistsortfullnamedesc', 'theme_boost_union'),
+ smartmenu_item::LISTSORT_SHORTNAME_ASC =>
+ get_string('smartmenusmenuitemlistsortshortnameasc', 'theme_boost_union'),
+ smartmenu_item::LISTSORT_SHORTNAME_DESC =>
+ get_string('smartmenusmenuitemlistsortshortnamedesc', 'theme_boost_union'),
+ smartmenu_item::LISTSORT_COURSEID_ASC =>
+ get_string('smartmenusmenuitemlistsortcourseidasc', 'theme_boost_union'),
+ smartmenu_item::LISTSORT_COURSEID_DESC =>
+ get_string('smartmenusmenuitemlistsortcourseiddesc', 'theme_boost_union'),
+ smartmenu_item::LISTSORT_COURSEIDNUMBER_ASC =>
+ get_string('smartmenusmenuitemlistsortcourseidnumberasc', 'theme_boost_union'),
+ smartmenu_item::LISTSORT_COURSEIDNUMBER_DESC =>
+ get_string('smartmenusmenuitemlistsortcourseidnumberdesc', 'theme_boost_union'),
+ ];
+ $mform->addElement('select', 'listsort',
+ get_string('smartmenusmenuitemtypedynamiccourses', 'theme_boost_union').': '.
+ get_string('smartmenusmenuitemlistsort', 'theme_boost_union'), $listsortoptions);
+ $mform->setDefault('listsort', smartmenu_item::LISTSORT_FULLNAME_ASC);
+ $mform->setType('listsort', PARAM_INT);
+ $mform->hideIf('listsort', 'type', 'neq', smartmenu_item::TYPEDYNAMIC);
+ $mform->addHelpButton('listsort', 'smartmenusmenuitemlistsort', 'theme_boost_union');
+
// Add course name presentation (for the dynamic courses menu item type) as select element.
$displayfieldoptions = [
smartmenu_item::FIELD_FULLNAME => get_string('smartmenusmenuitemdisplayfieldcoursefullname', 'theme_boost_union'),
diff --git a/classes/smartmenu_item.php b/classes/smartmenu_item.php
index 84647079a01..47bf4bd1e70 100644
--- a/classes/smartmenu_item.php
+++ b/classes/smartmenu_item.php
@@ -180,6 +180,54 @@ class smartmenu_item {
*/
const FIELD_FULLNAME = 0;
+ /**
+ * Sort the course list alphabetically by fullname ascending for dynamic menu item.
+ * @var int
+ */
+ const LISTSORT_FULLNAME_ASC = 0;
+
+ /**
+ * Sort the course list alphabetically by fullname descending for dynamic menu item.
+ * @var int
+ */
+ const LISTSORT_FULLNAME_DESC = 1;
+
+ /**
+ * Sort the course list alphabetically by shortname ascending for dynamic menu item.
+ * @var int
+ */
+ const LISTSORT_SHORTNAME_ASC = 2;
+
+ /**
+ * Sort the course list alphabetically by shortname descending for dynamic menu item.
+ * @var int
+ */
+ const LISTSORT_SHORTNAME_DESC = 3;
+
+ /**
+ * Sort the course list numerically by course-id ascending for dynamic menu item.
+ * @var int
+ */
+ const LISTSORT_COURSEID_ASC = 4;
+
+ /**
+ * Sort the course list numerically by course-id descending for dynamic menu item.
+ * @var int
+ */
+ const LISTSORT_COURSEID_DESC = 5;
+
+ /**
+ * Sort the course list alphabetically by course idnumber ascending for dynamic menu item.
+ * @var int
+ */
+ const LISTSORT_COURSEIDNUMBER_ASC = 6;
+
+ /**
+ * Sort the course list alphabetically by course idnumber descending for dynamic menu item.
+ * @var int
+ */
+ const LISTSORT_COURSEIDNUMBER_DESC = 7;
+
/**
* The ID of the menu item.
* @var int
@@ -641,8 +689,11 @@ protected function generate_dynamic_item() {
$sql = " SELECT $select FROM {course} c $join";
$sql .= $where ? " WHERE $where " : '';
- // Sort the courses in ascending order by its display field.
- $sql .= ($this->item->displayfield == self::FIELD_SHORTNAME) ? " ORDER BY c.shortname ASC " : " ORDER BY c.fullname ASC ";
+ // Sort the courses in ascending order by its ID.
+ // The real list sorting is done later as we have to handle multilanguage strings
+ // which is not possible in SQL.
+ // Sorting by ID here is just to make cases where two courses have exactly identical names deterministic.
+ $sql .= " ORDER BY c.id ASC ";
// Fetch the course records based on the sql.
$records = $DB->get_records_sql($sql, $params);
@@ -662,10 +713,48 @@ protected function generate_dynamic_item() {
$coursename = ($this->item->displayfield == self::FIELD_SHORTNAME) ? $record->shortname : $record->fullname;
// Short the course text name. used custom end (2) dots instead of three dots to display more words from coursenames.
$coursename = ($this->item->textcount) ? $this->shorten_words($coursename, $this->item->textcount) : $coursename;
+ // Store the string which should be used for sorting within the item.
+ switch ($this->item->listsort) {
+ case self::LISTSORT_FULLNAME_ASC:
+ case self::LISTSORT_FULLNAME_DESC:
+ default:
+ $sortstring = $record->fullname;
+ break;
+ case self::LISTSORT_SHORTNAME_ASC:
+ case self::LISTSORT_SHORTNAME_DESC:
+ $sortstring = $record->shortname;
+ break;
+ case self::LISTSORT_COURSEID_ASC:
+ case self::LISTSORT_COURSEID_DESC:
+ $sortstring = $record->id;
+ break;
+ case self::LISTSORT_COURSEIDNUMBER_ASC:
+ case self::LISTSORT_COURSEIDNUMBER_DESC:
+ $sortstring = $record->idnumber;
+ break;
+ }
- $items[] = $this->generate_node_data($coursename, $url, $rkey, null, 'link', false, [], $itemimage);
+ $items[] = $this->generate_node_data($coursename, $url, $rkey, null, 'link', false, [], $itemimage, $sortstring);
}
+ // Sort the courses based on the configured setting.
+ $listsort = $this->item->listsort;
+ usort($items, function($course1, $course2) use ($listsort) {
+ switch ($listsort) {
+ case self::LISTSORT_FULLNAME_ASC:
+ case self::LISTSORT_SHORTNAME_ASC:
+ case self::LISTSORT_COURSEID_ASC:
+ case self::LISTSORT_COURSEIDNUMBER_ASC:
+ default:
+ return strnatcasecmp($course1['sortstring'], $course2['sortstring']);
+ case self::LISTSORT_FULLNAME_DESC:
+ case self::LISTSORT_SHORTNAME_DESC:
+ case self::LISTSORT_COURSEID_DESC:
+ case self::LISTSORT_COURSEIDNUMBER_DESC:
+ return strnatcasecmp($course2['sortstring'], $course1['sortstring']);
+ }
+ });
+
// Submenu only contains the title as separate node.
if ($this->item->mode == self::MODE_SUBMENU) {
$haschildren = (count($items) > 0 ) ? true : false;
@@ -997,11 +1086,12 @@ public function build() {
* @param int $haschildren Whether the item has children or not, defaults to 0.
* @param array $children An array of child nodes, defaults to an empty array.
* @param string $itemimage Card image url for item.
+ * @param string $sortstring The string to be used for sorting the items.
*
* @return array An associative array of node data for the item.
*/
public function generate_node_data($title, $url, $key=null, $tooltip=null,
- $itemtype='link', $haschildren=0, $children=[], $itemimage='') {
+ $itemtype='link', $haschildren=0, $children=[], $itemimage='', $sortstring='') {
global $OUTPUT;
@@ -1048,6 +1138,7 @@ public function generate_node_data($title, $url, $key=null, $tooltip=null,
'itemtype' => 'link',
'link' => 1,
'sort' => uniqid(), // Support third level menu.
+ 'sortstring' => format_string($sortstring),
];
if ($haschildren && !empty($children)) {
diff --git a/db/install.xml b/db/install.xml
index 34e9ead2664..6f39b1f6393 100644
--- a/db/install.xml
+++ b/db/install.xml
@@ -68,6 +68,7 @@
+
diff --git a/db/upgrade.php b/db/upgrade.php
index 1366806f2eb..f4e21c4f1d1 100644
--- a/db/upgrade.php
+++ b/db/upgrade.php
@@ -262,5 +262,21 @@ function xmldb_theme_boost_union_upgrade($oldversion) {
upgrade_plugin_savepoint(true, 2023090100, 'theme', 'boost_union');
}
+ if ($oldversion < 2023102008) {
+ // Define table theme_boost_union_menus to be altered.
+ $table = new xmldb_table('theme_boost_union_menuitems');
+
+ // Define field listsort to be added to theme_boost_union_menuitems.
+ $field = new xmldb_field('listsort', XMLDB_TYPE_INTEGER, '9', null, null, null, null, 'customfields');
+
+ // Conditionally launch add field listsort.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ // Boost_union savepoint reached.
+ upgrade_plugin_savepoint(true, 2023102008, 'theme', 'boost_union');
+ }
+
return true;
}
diff --git a/lang/en/theme_boost_union.php b/lang/en/theme_boost_union.php
index 8fad7b49dda..4dd8ba5e8ce 100644
--- a/lang/en/theme_boost_union.php
+++ b/lang/en/theme_boost_union.php
@@ -880,6 +880,16 @@
$string['smartmenusmenuitemcssclass_help'] = 'Enter a CSS class for the menu item. This can be used to apply custom styling to the menu item.';
$string['smartmenusmenuitemdeleteconfirm'] = 'Are you sure you want to delete this menu item from the smart menu?';
$string['smartmenusmenuitemdeletesuccess'] = 'Smart menu item deleted successfully';
+$string['smartmenusmenuitemlistsort'] = 'Course list sorting';
+$string['smartmenusmenuitemlistsort_help'] = 'The course list will be sorted by the selected criteria and sort order. Choose between fullname, shortname, course ID and course ID number as criteria in combination with ascending and descending sort order.';
+$string['smartmenusmenuitemlistsortfullnameasc'] = 'Course fullname ascending';
+$string['smartmenusmenuitemlistsortfullnamedesc'] = 'Course fullname descending';
+$string['smartmenusmenuitemlistsortshortnameasc'] = 'Course shortname ascending';
+$string['smartmenusmenuitemlistsortshortnamedesc'] = 'Course shortname descending';
+$string['smartmenusmenuitemlistsortcourseidasc'] = 'Course ID ascending';
+$string['smartmenusmenuitemlistsortcourseiddesc'] = 'Course ID descending';
+$string['smartmenusmenuitemlistsortcourseidnumberasc'] = 'Course ID number ascending';
+$string['smartmenusmenuitemlistsortcourseidnumberdesc'] = 'Course ID number descending';
$string['smartmenusmenuitemdisplayfield'] = 'Course name presentation';
$string['smartmenusmenuitemdisplayfield_help'] = 'The course name which will be used as the title of the dynamic courses menu items. Choose between course full name and course short name';
$string['smartmenusmenuitemdisplayfieldcoursefullname'] = 'Course full name';
diff --git a/tests/behat/theme_boost_union_smartmenusettings_menuitems_dynamiccourses.feature b/tests/behat/theme_boost_union_smartmenusettings_menuitems_dynamiccourses.feature
index a484c41f80a..2655858a147 100644
--- a/tests/behat/theme_boost_union_smartmenusettings_menuitems_dynamiccourses.feature
+++ b/tests/behat/theme_boost_union_smartmenusettings_menuitems_dynamiccourses.feature
@@ -41,6 +41,7 @@ Feature: Configuring the theme_boost_union plugin on the "Smart menus" page, usi
And I create smart menu with the following fields to these values:
| Title | List menu |
| Menu location(s) | Main, Menu, User, Bottom |
+ | CSS class | dynamiccoursetest |
And I set "List menu" smart menu items with the following fields to these values:
| Title | Dynamic courses |
| Menu item type | Dynamic courses |
@@ -225,3 +226,40 @@ Feature: Configuring the theme_boost_union plugin on the "Smart menus" page, usi
| value | user | course1 | course2 | course3 | course4 |
| value1 | student1 | should | should | should not | should not |
| value2 | student1 | should not | should not | should | should not |
+
+ @javascript
+ Scenario Outline: Smartmenus: Menu items: Dynamic courses - Sort the course list based on the given setting
+ Given the following "courses" exist:
+ | fullname | shortname | category | idnumber |
+ | AAA Course | BBB | CAT1 | CCC |
+ | BBB Course | AAA | CAT1 | BBB |
+ | CCC Course | CCC | CAT1 | AAA |
+ When I log in as "admin"
+ And I navigate to smart menu "List menu" items
+ And I click on ".action-edit" "css_element" in the "Dynamic courses" "table_row"
+ And I set the field "Dynamic courses: Course list sorting" to ""
+ And I press "Save changes"
+ And I log out
+ When I log in as "student1"
+ And I click on ".dynamiccoursetest" "css_element"
+ Then "" "text" should appear before "" "text" in the ".dynamiccoursetest .dropdown-menu" "css_element"
+ And "" "text" should appear before "" "text" in the ".dynamiccoursetest .dropdown-menu" "css_element"
+
+ Examples:
+ | sorting | thisbeforethat1 | thisbeforethat2 | thisbeforethat3 |
+ # Option: Course fullname ascending
+ | 0 | AAA Course | BBB Course | CCC Course |
+ # Option: Course fullname descending
+ | 1 | CCC Course | BBB Course | AAA Course |
+ # Option: Course shortname ascending
+ | 2 | BBB Course | AAA Course | CCC Course |
+ # Option: Course shortname descending
+ | 3 | CCC Course | AAA Course | BBB Course |
+ # Option: Course ID ascending
+ | 4 | AAA Course | BBB Course | CCC Course |
+ # Option: Course ID descending
+ | 5 | CCC Course | BBB Course | AAA Course |
+ # Option: Course ID number ascending
+ | 6 | CCC Course | BBB Course | AAA Course |
+ # Option: Course ID number descending
+ | 7 | AAA Course | BBB Course | CCC Course |
diff --git a/version.php b/version.php
index a2356d4a6a9..02fedaeb009 100644
--- a/version.php
+++ b/version.php
@@ -25,7 +25,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'theme_boost_union';
-$plugin->version = 2023102007;
+$plugin->version = 2023102008;
$plugin->release = 'v4.3-r3';
$plugin->requires = 2023100900;
$plugin->supported = [403, 403];