diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5ec5249932..6b658341f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ Versioning](https://semver.org/spec/v2.0.0.html).
### Added
+- Added new page extension `MainMenuEntry`.
- Grid options for the Section plugin
- Make user waive its right of withdrawal when purchasing
a course product relation with `is_withdrawable` set to `false`
diff --git a/cookiecutter/{{cookiecutter.organization}}-richie-site-factory/template/{{cookiecutter.site}}/src/backend/{{cookiecutter.site}}/settings.py b/cookiecutter/{{cookiecutter.organization}}-richie-site-factory/template/{{cookiecutter.site}}/src/backend/{{cookiecutter.site}}/settings.py
index b54d9ce7c2..518a0fff2b 100644
--- a/cookiecutter/{{cookiecutter.organization}}-richie-site-factory/template/{{cookiecutter.site}}/src/backend/{{cookiecutter.site}}/settings.py
+++ b/cookiecutter/{{cookiecutter.organization}}-richie-site-factory/template/{{cookiecutter.site}}/src/backend/{{cookiecutter.site}}/settings.py
@@ -659,6 +659,13 @@ class Base(StyleguideMixin, DRFMixin, RichieCoursesConfigurationMixin, Configura
"body > svg, #main-menu, .body-footer, .body-mentions"
)
+ # Wheither you can create PageIndex extension on page through toolbar if true or
+ # just editing existing extension if false
+ RICHIE_MAINMENUENTRY_ALLOW_CREATION = False
+
+ # Define which node level can be processed to search for pageindex extension
+ RICHIE_MAINMENUENTRY_MENU_ALLOWED_LEVEL = 0
+
# pylint: disable=invalid-name
@property
def ENVIRONMENT(self):
diff --git a/sandbox/settings.py b/sandbox/settings.py
index b584e5a304..787e3edd07 100644
--- a/sandbox/settings.py
+++ b/sandbox/settings.py
@@ -598,6 +598,13 @@ class Base(StyleguideMixin, DRFMixin, RichieCoursesConfigurationMixin, Configura
environ_prefix=None,
)
+ # Wheither you can create PageIndex extension on page through toolbar if true or
+ # just editing existing extension if false
+ RICHIE_MAINMENUENTRY_ALLOW_CREATION = False
+
+ # Define which node level can be processed to search for pageindex extension
+ RICHIE_MAINMENUENTRY_MENU_ALLOWED_LEVEL = 0
+
@classmethod
def _get_environment(cls):
"""Environment in which the application is launched."""
diff --git a/src/frontend/scss/components/_header.scss b/src/frontend/scss/components/_header.scss
index c96a37181d..93f0a654ed 100644
--- a/src/frontend/scss/components/_header.scss
+++ b/src/frontend/scss/components/_header.scss
@@ -208,6 +208,18 @@
@include sv-flex(0, 0, auto);
}
+ // Define color variable for default item color on hover then available variants
+ @if r-theme-val(topbar, item-hover-color) {
+ --r--menu--item--hover--color: #{r-theme-val(topbar, item-hover-color)};
+ }
+ // WARNING: Those are currently some variant samples during development
+ &--primary {
+ --r--menu--item--hover--color: #{$primary};
+ }
+ &--warning {
+ --r--menu--item--hover--color: #{$warning};
+ }
+
& > a {
@include sv-flex(1, 0, 100%);
display: flex;
@@ -228,6 +240,7 @@
@include media-breakpoint-up($r-topbar-breakpoint) {
position: relative;
+ // If there is no default hover color we assume there is also no variant
@if r-theme-val(topbar, item-hover-color) {
&::after {
content: '';
@@ -236,7 +249,7 @@
left: 0;
right: 0;
height: 8px;
- background-color: r-theme-val(topbar, item-hover-color);
+ background-color: var(--r--menu--item--hover--color);
border-top-left-radius: 0.2rem;
border-top-right-radius: 0.2rem;
}
diff --git a/src/richie/apps/core/templates/menu/header_menu.html b/src/richie/apps/core/templates/menu/header_menu.html
index 96bb7d72a4..9289d9e7a7 100644
--- a/src/richie/apps/core/templates/menu/header_menu.html
+++ b/src/richie/apps/core/templates/menu/header_menu.html
@@ -1,19 +1,23 @@
-{% load cms_tags %}{% spaceless %}
+{% load menu_tags %}{% spaceless %}
{% for child in children %}
- {% with children_slug=child.get_menu_title|slugify %}
-
{% endwith %}
{% endfor %}
diff --git a/src/richie/apps/core/templates/richie/base.html b/src/richie/apps/core/templates/richie/base.html
index daacadf5ba..d38c7c2757 100644
--- a/src/richie/apps/core/templates/richie/base.html
+++ b/src/richie/apps/core/templates/richie/base.html
@@ -135,7 +135,7 @@
diff --git a/src/richie/apps/courses/admin.py b/src/richie/apps/courses/admin.py
index 1378b8972d..4b7c3029ba 100644
--- a/src/richie/apps/courses/admin.py
+++ b/src/richie/apps/courses/admin.py
@@ -280,6 +280,21 @@ def snapshot(self, request, course_id, *args, **kwargs):
return JsonResponse({"id": new_page.course.id})
+class MainMenuEntryAdmin(PageExtensionAdmin):
+ """
+ Admin class for the MainMenuEntry model
+ """
+
+ list_display = ["title", "allow_submenu"]
+
+ # pylint: disable=no-self-use
+ def title(self, obj):
+ """
+ Get the page title from the related page
+ """
+ return obj.extended_object.get_title()
+
+
class OrganizationAdmin(PageExtensionAdmin):
"""
Admin class for the Organization model
@@ -349,6 +364,7 @@ class LicenceAdmin(TranslatableAdmin):
admin.site.register(models.Course, CourseAdmin)
admin.site.register(models.CourseRun, CourseRunAdmin)
admin.site.register(models.Licence, LicenceAdmin)
+admin.site.register(models.MainMenuEntry, MainMenuEntryAdmin)
admin.site.register(models.Organization, OrganizationAdmin)
admin.site.register(models.PageRole, PageRoleAdmin)
admin.site.register(models.Person, PersonAdmin)
diff --git a/src/richie/apps/courses/cms_menus.py b/src/richie/apps/courses/cms_menus.py
new file mode 100644
index 0000000000..37c5d0a66f
--- /dev/null
+++ b/src/richie/apps/courses/cms_menus.py
@@ -0,0 +1,79 @@
+"""
+Menu modifier to add feature for menu template context.
+"""
+
+from django.conf import settings
+
+from menus.base import Modifier
+from menus.menu_pool import menu_pool
+
+from .models import MainMenuEntry
+
+
+class MenuWithMainMenuEntry(Modifier):
+ """
+ Menu modifier to include MainMenuEntry extension data in menu template context.
+
+ In menu template you will be able to reach possible extension data from node
+ attribute ``menu_extension``. If node page has no extension it will have an empty
+ dict. Only a specific node level is processed and nodes with a different level
+ won't have the attribute ``menu_extension`` at all.
+ """
+
+ # pylint: disable=too-many-arguments,too-many-positional-arguments
+ def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb):
+ """
+ Patch navigation nodes to include data from possible extension
+ ``MainMenuEntry``.
+
+ For performance:
+
+ * This does not work for breadcrumb navigation (all extension options are mean
+ for menu only);
+ * This works only on the menu top level, it means the one defined as first
+ argument from tag ``{% show_menu .. %}``;
+
+ Then to avoid making a query for each node item to retrieve its possible
+ extension object, we get the extensions in bulk as values instead of objects.
+
+ Finally we add the data on nodes so they can used from menu template.
+ """
+ # We are not altering breadcrumb menu, this is only for navigation menu and
+ # only for the visible menu (not the whole processed tree)
+ if not nodes or breadcrumb or not post_cut:
+ return nodes
+
+ # Get the page ids to process, only for the allowed node level
+ page_ids = [
+ node.id
+ for node in nodes
+ if node.level == settings.RICHIE_MAINMENUENTRY_MENU_ALLOWED_LEVEL
+ ]
+
+ # No need to continue if we don't have any valid node
+ if not page_ids:
+ return nodes
+
+ # We directly get the extensions from their related page id and serialized
+ # as a dict instead of model object
+ extension_queryset = MainMenuEntry.objects.filter(
+ extended_object_id__in=page_ids
+ ).values("extended_object_id", "allow_submenu", "menu_color")
+
+ # Pack extensions data into proper structure
+ extension_datas = {
+ item["extended_object_id"]: {
+ "allow_submenu": item["allow_submenu"],
+ "menu_color": item["menu_color"],
+ }
+ for item in extension_queryset
+ }
+
+ # Attach each possible extension data to its relative node
+ for node in nodes:
+ node.menu_extension = extension_datas.get(node.id, {})
+
+ return nodes
+
+
+menu_pool.register_modifier(MenuWithMainMenuEntry)
diff --git a/src/richie/apps/courses/cms_toolbars.py b/src/richie/apps/courses/cms_toolbars.py
index 66dac30f43..1797282c18 100644
--- a/src/richie/apps/courses/cms_toolbars.py
+++ b/src/richie/apps/courses/cms_toolbars.py
@@ -2,6 +2,7 @@
Toolbar extension for the courses application
"""
+from django.conf import settings
from django.utils.text import capfirst
from django.utils.translation import gettext_lazy as _
@@ -12,7 +13,7 @@
from cms.utils.urlutils import admin_reverse
from .defaults import PAGE_EXTENSION_TOOLBAR_ITEM_POSITION
-from .models import Category, Course, Organization, Person
+from .models import Category, Course, MainMenuEntry, Organization, Person
class BaseExtensionToolbar(ExtensionToolbar):
@@ -131,3 +132,52 @@ class PersonExtensionToolbar(BaseExtensionToolbar):
"""
model = Person
+
+
+@toolbar_pool.register
+class MainMenuEntryExtensionToolbar(BaseExtensionToolbar):
+ """
+ This extension class customizes the toolbar for the MainMenuEntry page extension.
+ """
+
+ model = MainMenuEntry
+
+ def populate(self):
+ """
+ Specific extension populate method.
+
+ This extension entry only appears in toolbar if page already have extension or
+ if setting ``RICHIE_MAINMENUENTRY_ALLOW_CREATION`` is true. Finally the page
+ level must also match the allowed level from setting
+ ``RICHIE_MAINMENUENTRY_MENU_ALLOWED_LEVEL``.
+ """
+ # always use draft if we have a page
+ self.page = get_page_draft(self.request.current_page)
+ if not self.page:
+ # Nothing to do
+ return
+
+ # setup the extension toolbar with permissions and sanity checks
+ page_menu = self._setup_extension_toolbar()
+
+ if user_can_change_page(user=self.request.user, page=self.page):
+ # Retrieves extension instance (if any) and toolbar URL
+ page_extension, admin_url = self.get_page_extension_admin()
+ # Get the page node level
+ level = self.page.node.get_depth() - 1
+ allowed = page_extension is not None or (
+ page_extension is None
+ and settings.RICHIE_MAINMENUENTRY_ALLOW_CREATION is True
+ )
+ if (
+ allowed
+ and level == settings.RICHIE_MAINMENUENTRY_MENU_ALLOWED_LEVEL
+ and admin_url
+ ):
+ # Adds a toolbar item in position 0 (at the top of the menu)
+ page_menu.add_modal_item(
+ _("Main menu settings"),
+ url=admin_url,
+ disabled=not self.toolbar.edit_mode_active,
+ position=PAGE_EXTENSION_TOOLBAR_ITEM_POSITION,
+ )
diff --git a/src/richie/apps/courses/defaults.py b/src/richie/apps/courses/defaults.py
index bea2f7d631..ae542ac420 100644
--- a/src/richie/apps/courses/defaults.py
+++ b/src/richie/apps/courses/defaults.py
@@ -312,6 +312,10 @@
"reverse_id": "organizations",
"template": "courses/cms/organization_detail.html",
}
+MENUENTRIES_PAGE = {
+ "reverse_id": None,
+ "template": "richie/single_column.html",
+}
PERSONS_PAGE = {"reverse_id": "persons", "template": "courses/cms/person_detail.html"}
PROGRAMS_PAGE = {
"reverse_id": "programs",
@@ -382,3 +386,11 @@
# Maximum number of archived course runs displayed by default on course detail page.
# The additional runs can be viewed by clicking on `View more` link.
RICHIE_MAX_ARCHIVED_COURSE_RUNS = 10
+
+# Define possible hover color that can be choosen for an MainMenuEntry and to apply on
+# its menu item
+MENU_ENTRY_COLOR_CLASSES = getattr(
+ settings,
+ "RICHIE_MENU_ENTRY_COLOR_CLASSES",
+ (("", _("None")),),
+)
diff --git a/src/richie/apps/courses/factories.py b/src/richie/apps/courses/factories.py
index be9b319d05..d428151442 100644
--- a/src/richie/apps/courses/factories.py
+++ b/src/richie/apps/courses/factories.py
@@ -910,3 +910,26 @@ def fill_excerpt(self, create, extracted, **kwargs):
plugin_type="PlainTextPlugin",
body=text,
)
+
+
+class MainMenuEntryFactory(BLDPageExtensionDjangoModelFactory):
+ """
+ A factory to automatically generate random yet meaningful menu entry page extensions
+ and their related page in our tests.
+ """
+
+ class Meta:
+ model = models.MainMenuEntry
+ exclude = [
+ "page_in_navigation",
+ "page_languages",
+ "page_parent",
+ "page_reverse_id",
+ "page_template",
+ "page_title",
+ ]
+
+ # fields concerning the related page
+ page_template = models.MainMenuEntry.PAGE["template"]
+ allow_submenu = False
+ menu_color = ""
diff --git a/src/richie/apps/courses/migrations/0035_add_menuentry.py b/src/richie/apps/courses/migrations/0035_add_menuentry.py
new file mode 100644
index 0000000000..2f3b8eec5b
--- /dev/null
+++ b/src/richie/apps/courses/migrations/0035_add_menuentry.py
@@ -0,0 +1,74 @@
+# Generated by Django 4.2.16 on 2024-09-19 00:24
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+from ..defaults import MENU_ENTRY_COLOR_CLASSES
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("cms", "0022_auto_20180620_1551"),
+ ("courses", "0034_auto_20230817_1736"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="MainMenuEntry",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "allow_submenu",
+ models.BooleanField(
+ default=False,
+ help_text="If enabled the page entry in menu will be a dropdown for its possible children.",
+ verbose_name="Allow submenu",
+ ),
+ ),
+ (
+ "menu_color",
+ models.CharField(
+ blank=True,
+ choices=MENU_ENTRY_COLOR_CLASSES,
+ default="",
+ help_text="A color used to display page entry in menu.",
+ max_length=10,
+ verbose_name="Color in menu",
+ ),
+ ),
+ (
+ "extended_object",
+ models.OneToOneField(
+ editable=False,
+ on_delete=django.db.models.deletion.CASCADE,
+ to="cms.page",
+ ),
+ ),
+ (
+ "public_extension",
+ models.OneToOneField(
+ editable=False,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="draft_extension",
+ to="courses.mainmenuentry",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "main menu entry",
+ "verbose_name_plural": "main menu entries",
+ "db_table": "richie_menuentry",
+ "ordering": ["-pk"],
+ },
+ ),
+ ]
diff --git a/src/richie/apps/courses/models/__init__.py b/src/richie/apps/courses/models/__init__.py
index 15b1903e68..9edc58aa4f 100644
--- a/src/richie/apps/courses/models/__init__.py
+++ b/src/richie/apps/courses/models/__init__.py
@@ -6,6 +6,7 @@
from .blog import *
from .category import *
from .course import *
+from .menuentry import *
from .organization import *
from .person import *
from .program import *
diff --git a/src/richie/apps/courses/models/menuentry.py b/src/richie/apps/courses/models/menuentry.py
new file mode 100644
index 0000000000..cdbccf2ece
--- /dev/null
+++ b/src/richie/apps/courses/models/menuentry.py
@@ -0,0 +1,51 @@
+"""
+Declare and configure the models for the menu entry part
+"""
+
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from cms.extensions.extension_pool import extension_pool
+
+from ...core.models import BasePageExtension
+from .. import defaults
+
+
+class MainMenuEntry(BasePageExtension):
+ """
+ The MainMenuEntry extension defines some options for a page entry in the main menu.
+ """
+
+ PAGE = defaults.MENUENTRIES_PAGE
+
+ class Meta:
+ db_table = "richie_menuentry"
+ ordering = ["-pk"]
+ verbose_name = _("main menu entry")
+ verbose_name_plural = _("main menu entries")
+
+ def __str__(self):
+ """Human representation of an main menu entry page"""
+ model = self._meta.verbose_name.title()
+ name = self.extended_object.get_title()
+ return f"{model:s}: {name:s}"
+
+ allow_submenu = models.BooleanField(
+ _("Allow submenu"),
+ default=False,
+ help_text=_(
+ "If enabled the page entry in menu will be a dropdown for its possible "
+ "children."
+ ),
+ )
+ menu_color = models.CharField(
+ _("Color in menu"),
+ max_length=10,
+ default="",
+ blank=True,
+ choices=defaults.MENU_ENTRY_COLOR_CLASSES,
+ help_text=_("A color used to display page entry in menu."),
+ )
+
+
+extension_pool.register(MainMenuEntry)
diff --git a/src/richie/apps/courses/settings/__init__.py b/src/richie/apps/courses/settings/__init__.py
index 877e0f10b4..d9203c74e2 100644
--- a/src/richie/apps/courses/settings/__init__.py
+++ b/src/richie/apps/courses/settings/__init__.py
@@ -578,3 +578,14 @@ def richie_placeholder_conf(name):
"sizes": "60px",
},
}
+
+# If true the toolbar item will already be showed. If false only a page which already
+# have the extension will have the toolbar item and users won't be able to add
+# MainMenuEntry extension on existing page, only create new page with index extension
+# through the wizard.
+RICHIE_MAINMENUENTRY_ALLOW_CREATION = False
+
+# Define which node level can be processed to search for MainMenuEntry extension. You
+# can set it to 'None' for never processing any node.
+# This is a limit against performance issues to avoid making querysets for nothing.
+RICHIE_MAINMENUENTRY_MENU_ALLOWED_LEVEL = 0
diff --git a/tests/apps/courses/test_cms_toolbars.py b/tests/apps/courses/test_cms_toolbars.py
index 2bdc03fd0f..b08fc411a7 100644
--- a/tests/apps/courses/test_cms_toolbars.py
+++ b/tests/apps/courses/test_cms_toolbars.py
@@ -14,6 +14,7 @@
from richie.apps.core.factories import UserFactory
from richie.apps.courses.factories import (
CourseFactory,
+ MainMenuEntryFactory,
OrganizationFactory,
PersonFactory,
)
@@ -211,6 +212,114 @@ def test_cms_toolbars_no_page_extension(self):
results = page_menu.find_items(ModalItem, name="Person settings...")
self.assertEqual(results, [])
+ # Check that the index page item is absent with default settings
+ results = page_menu.find_items(ModalItem, name="Main menu settings...")
+ self.assertEqual(results, [])
+
+ @override_settings(CMS_PERMISSION=False)
+ def test_cms_toolbars_menuentry_extension_availability(self):
+ """
+ MainMenuEntry extension has advanced toolbar behaviors depending from settings.
+ """
+ # Testing with a superuser proves our point
+ superuser = UserFactory(is_staff=True, is_superuser=True)
+
+ # Create a page not related to any page extension
+ page = create_page(
+ "A page", template="richie/single_column.html", language="en"
+ )
+ # Create a page on level 0 and with existing extension
+ menuentry = MainMenuEntryFactory(page_parent=page)
+
+ cases = [[False, False], [False, True], [True, False]]
+
+ for args in cases:
+ # Extension should not be created from toolbar for any level but could be
+ # edited from tree level 0
+ with self.settings(
+ RICHIE_MAINMENUENTRY_ALLOW_CREATION=False,
+ RICHIE_MAINMENUENTRY_MENU_ALLOWED_LEVEL=0,
+ ):
+ toolbar = self.get_toolbar_for_page(page, superuser, *args)
+ page_menu = toolbar.find_items(Menu, name="Page")[0].item
+
+ # menuentry page entry is present
+ results = page_menu.find_items(ModalItem, name="Main menu settings...")
+ self.assertEqual(results, [])
+
+ toolbar = self.get_toolbar_for_page(
+ menuentry.extended_object, superuser, *args
+ )
+ page_menu = toolbar.find_items(Menu, name="Page")[0].item
+
+ # menuentry page entry is present
+ results = page_menu.find_items(ModalItem, name="Main menu settings...")
+ self.assertEqual(results, [])
+
+ # Extension should not be created from toolbar for any level but could be
+ # edited from tree level 1
+ with self.settings(
+ RICHIE_MAINMENUENTRY_ALLOW_CREATION=False,
+ RICHIE_MAINMENUENTRY_MENU_ALLOWED_LEVEL=1,
+ ):
+ toolbar = self.get_toolbar_for_page(page, superuser, *args)
+ page_menu = toolbar.find_items(Menu, name="Page")[0].item
+
+ # menuentry page entry is present
+ results = page_menu.find_items(ModalItem, name="Main menu settings...")
+ self.assertEqual(results, [])
+
+ toolbar = self.get_toolbar_for_page(
+ menuentry.extended_object, superuser, *args
+ )
+ page_menu = toolbar.find_items(Menu, name="Page")[0].item
+
+ # menuentry page entry is present
+ results = page_menu.find_items(ModalItem, name="Main menu settings...")
+ self.assertEqual(len(results), 1)
+
+ # Extension should be created or edited from toolbar for tree level 0
+ with self.settings(
+ RICHIE_MAINMENUENTRY_ALLOW_CREATION=True,
+ RICHIE_MAINMENUENTRY_MENU_ALLOWED_LEVEL=0,
+ ):
+ toolbar = self.get_toolbar_for_page(page, superuser, *args)
+ page_menu = toolbar.find_items(Menu, name="Page")[0].item
+
+ # menuentry page entry is present
+ results = page_menu.find_items(ModalItem, name="Main menu settings...")
+ self.assertEqual(len(results), 1)
+
+ toolbar = self.get_toolbar_for_page(
+ menuentry.extended_object, superuser, *args
+ )
+ page_menu = toolbar.find_items(Menu, name="Page")[0].item
+
+ # menuentry page entry is present
+ results = page_menu.find_items(ModalItem, name="Main menu settings...")
+ self.assertEqual(results, [])
+
+ # Extension should be created or edited from toolbar for tree level 1
+ with self.settings(
+ RICHIE_MAINMENUENTRY_ALLOW_CREATION=True,
+ RICHIE_MAINMENUENTRY_MENU_ALLOWED_LEVEL=1,
+ ):
+ toolbar = self.get_toolbar_for_page(page, superuser, *args)
+ page_menu = toolbar.find_items(Menu, name="Page")[0].item
+
+ # menuentry page entry is present
+ results = page_menu.find_items(ModalItem, name="Main menu settings...")
+ self.assertEqual(results, [])
+
+ toolbar = self.get_toolbar_for_page(
+ menuentry.extended_object, superuser, *args
+ )
+ page_menu = toolbar.find_items(Menu, name="Page")[0].item
+
+ # menuentry page entry is present
+ results = page_menu.find_items(ModalItem, name="Main menu settings...")
+ self.assertEqual(len(results), 1)
+
@override_settings(CMS_PERMISSION=False)
# pylint: disable=too-many-locals
def test_cms_toolbars_person_has_page_extension_settings_item(self):
@@ -262,3 +371,18 @@ def test_cms_toolbars_person_has_page_extension_settings_item(self):
item = method(toolbar, "Person settings...")
if item:
self.assertEqual(item.url, url)
+
+ @override_settings(CMS_PERMISSION=False)
+ def test_cms_toolbars_menuentry_has_page_extension_settings_item(self):
+ """
+ Validate that a new item to edit the menu entry is available only when
+ visiting the page in edit mode and for users with permission to edit the page.
+ """
+ menuentry = MainMenuEntryFactory()
+ url = f"/en/admin/courses/mainmenuentry/{menuentry.id:d}/change/"
+
+ for args, method in self.get_cases_for_page_change():
+ toolbar = self.get_toolbar_for_page(menuentry.extended_object, *args)
+ item = method(toolbar, "Main menu settings...")
+ if item:
+ self.assertEqual(item.url, url)