Skip to content

Commit 39906dd

Browse files
author
David Logie
committed
CDD-3119 Add a new SimpleMenu model.
This is a simplified version of the current Menu model where menus are now just simple links with a title.
1 parent fdb88c3 commit 39906dd

File tree

18 files changed

+516
-16
lines changed

18 files changed

+516
-16
lines changed

cms/dashboard/management/commands/build_cms_site_helpers/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@
1111
create_whats_new_child_entry,
1212
create_feedback_page,
1313
)
14-
from .menu import create_menu_snippet
14+
from .menu import create_menu_snippet, create_simplemenu_snippet

cms/dashboard/management/commands/build_cms_site_helpers/menu.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from cms.composite.models import CompositePage
33
from cms.home.models import LandingPage
44
from cms.metrics_documentation.models import MetricsDocumentationParentPage
5-
from cms.snippets.models import Menu
5+
from cms.snippets.models import Menu, SimpleMenu
66
from cms.topic.models import TopicPage
77
from cms.whats_new.models import WhatsNewParentPage
88

@@ -172,3 +172,33 @@ def _create_menu_data() -> list[dict]:
172172
"id": "dcd6d76c-a3b3-4b44-8326-8177d609b50b",
173173
}
174174
]
175+
176+
177+
def create_simplemenu_snippet():
178+
SimpleMenu.objects.create(
179+
internal_label="Primary navigation",
180+
is_active=True,
181+
body=_create_simplemenu_data(),
182+
)
183+
184+
185+
def _create_simplemenu_data() -> list[dict]:
186+
covid_page = TopicPage.objects.get(slug="covid-19")
187+
flu_page = TopicPage.objects.get(slug="influenza")
188+
189+
return [
190+
{
191+
"type": "link",
192+
"value": {"title": "COVID", "page": covid_page.id, "html_url": covid_page.full_url},
193+
"id": "d8e270c7-f3d7-41cf-8d7c-c2bbe62ed71d",
194+
},
195+
{
196+
"type": "link",
197+
"value": {
198+
"title": "What's new",
199+
"page": flu_page.id,
200+
"html_url": flu_page.full_url,
201+
},
202+
"id": "021352b9-d606-48ee-b942-1739ccec9e03",
203+
},
204+
]

cms/snippets/managers/menu.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,63 @@ def is_menu_overriding_currently_active_menu(self, menu) -> bool:
6161

6262
active_menu = self.get_active_menu()
6363
return bool(menu.is_active and menu != active_menu)
64+
65+
66+
class SimpleMenuQuerySet(models.QuerySet):
67+
"""Custom queryset which can be used by the `SimpleMenu"""
68+
69+
def get_active_menus(self) -> Self:
70+
"""Gets the all currently active `SimpleMenu`.
71+
72+
Returns:
73+
QuerySet: A queryset of the active banners:
74+
Examples:
75+
`<SimpleMenuQuerySet [<Menu>]>`
76+
"""
77+
return self.filter(is_active=True)
78+
79+
80+
class SimpleMenuManager(models.Manager):
81+
"""Custom model manager class for the `SimpleMenu` model"""
82+
83+
def get_queryset(self) -> SimpleMenuQuerySet:
84+
return SimpleMenuQuerySet(model=self.model, using=self.db)
85+
86+
def has_active_menu(self) -> bool:
87+
"""Checks if there is already a `SimpleMenu` which is active
88+
89+
Returns:
90+
True if there is a `SimpleMenu` which has `is_active` set to True.
91+
False otherwise.
92+
93+
"""
94+
return self.get_queryset().get_active_menus().exists()
95+
96+
def get_active_menu(self):
97+
"""Gets the currently active `SimpleMenu`.
98+
99+
Returns:
100+
The currently active `SimpleMenu` if available.
101+
If there is no `SimpleMenu` with `is_active` set to True,
102+
then None is returned.
103+
104+
"""
105+
return self.get_queryset().get_active_menus().first()
106+
107+
def is_menu_overriding_currently_active_menu(self, menu) -> bool:
108+
"""Determines if the given `menu` is trying to override an existing active `SimpleMenu`
109+
110+
Args:
111+
menu: The current `SimpleMenu` object which is being evaluated
112+
113+
Returns:
114+
True if the given `menu` is trying to override
115+
an existing active `SimpleMenu`. False otherwise.
116+
117+
"""
118+
has_existing_active_menu: bool = self.has_active_menu()
119+
if not has_existing_active_menu:
120+
return False
121+
122+
active_menu = self.get_active_menu()
123+
return bool(menu.is_active and menu != active_menu)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Generated by Django 5.2.11 on 2026-03-19 14:48
2+
3+
import django.db.models.deletion
4+
import wagtail.fields
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
("snippets", "0013_remove_geography_code_field_from_wha_button"),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name="SimpleMenu",
17+
fields=[
18+
(
19+
"id",
20+
models.BigAutoField(
21+
auto_created=True,
22+
primary_key=True,
23+
serialize=False,
24+
verbose_name="ID",
25+
),
26+
),
27+
(
28+
"internal_label",
29+
models.TextField(
30+
help_text="\nA label to associate with this particular menu design.\nNote that this label is private / internal and is not used on the dashboard.\nThis is purely to help identify each of the constructed menu designs.\n"
31+
),
32+
),
33+
(
34+
"is_active",
35+
models.BooleanField(
36+
default=False,
37+
help_text="\nWhether to activate this menu. \nNote that only 1 menu can be active at a time.\nTo switch from 1 active menu to another, \nyou must deactivate the 1st menu and save it before activating and saving the 2nd menu.\n",
38+
),
39+
),
40+
(
41+
"body",
42+
wagtail.fields.StreamField(
43+
[("link", 2)],
44+
block_lookup={
45+
0: (
46+
"wagtail.blocks.TextBlock",
47+
(),
48+
{
49+
"help_text": "\nThe title to display for this menu item.\nAs a general rule of thumb, the title length should be no longer than 60 characters.\n",
50+
"required": True,
51+
},
52+
),
53+
1: (
54+
"wagtail.blocks.PageChooserBlock",
55+
("wagtailcore.Page",),
56+
{
57+
"on_delete": django.db.models.deletion.CASCADE,
58+
"related_name": "+",
59+
},
60+
),
61+
2: (
62+
"wagtail.blocks.StructBlock",
63+
[[("title", 0), ("page", 1)]],
64+
{},
65+
),
66+
},
67+
help_text="\nThe menu is constructed from a grid system of rows and columns.\nThere can be any number of rows and columns.\nBut each column should have at least 1 link.\n",
68+
),
69+
),
70+
],
71+
),
72+
]

cms/snippets/models/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
from .external_button import ExternalButton, ExternalButtonTypes, ExternalButtonIcons
33
from .wha_button import WeatherAlertButton, WeatherAlertButtonTypes
44
from .global_banner import GlobalBanner
5-
from .menu_builder.menu import Menu
5+
from .menu_builder.menu import Menu, SimpleMenu
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .menu import Menu
1+
from .menu import Menu, SimpleMenu

cms/snippets/models/menu_builder/help_texts.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,11 @@
4545
Note that this label is private / internal and is not used on the dashboard.
4646
This is purely to help identify each of the constructed menu designs.
4747
"""
48+
49+
SIMPLEMENU_HELP_TEXT = """
50+
The title to display for this menu item.
51+
"""
52+
53+
SIMPLEMENU_BODY_TEXT = """
54+
Links to display in the menu.
55+
"""

cms/snippets/models/menu_builder/menu.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from django.core.exceptions import ValidationError
22
from django.db import models
3+
from wagtail import fields
34
from wagtail.admin.panels.field_panel import FieldPanel
45
from wagtail.snippets.models import register_snippet
56

6-
from cms.snippets.managers.menu import MenuManager
7+
from cms.snippets.managers.menu import MenuManager, SimpleMenuManager
78
from cms.snippets.models.menu_builder import help_texts
89
from cms.snippets.models.menu_builder.dynamic_content import ALLOWABLE_BODY_CONTENT
10+
from cms.snippets.models.menu_builder.menu_link import SimpleMenuLink
911

1012

1113
class MultipleMenusActiveError(ValidationError):
@@ -39,3 +41,28 @@ def clean(self) -> None:
3941
def _raise_error_if_trying_to_enable_multiple_menus(self) -> None:
4042
if Menu.objects.is_menu_overriding_currently_active_menu(menu=self):
4143
raise MultipleMenusActiveError
44+
45+
46+
@register_snippet
47+
class SimpleMenu(models.Model):
48+
internal_label = models.TextField(help_text=help_texts.MENU_INTERNAL_LABEL)
49+
is_active = models.BooleanField(default=False, help_text=help_texts.MENU_IS_ACTIVE)
50+
body = fields.StreamField(
51+
block_types=[("link", SimpleMenuLink())],
52+
use_json_field=True,
53+
help_text=help_texts.SIMPLEMENU_BODY_TEXT,
54+
)
55+
56+
objects = SimpleMenuManager()
57+
58+
def __str__(self) -> str:
59+
prefix = "Active" if self.is_active else "Inactive"
60+
return f"({prefix}) - {self.internal_label}"
61+
62+
def clean(self) -> None:
63+
super().clean()
64+
self._raise_error_if_trying_to_enable_multiple_menus()
65+
66+
def _raise_error_if_trying_to_enable_multiple_menus(self) -> None:
67+
if SimpleMenu.objects.is_menu_overriding_currently_active_menu(menu=self):
68+
raise MultipleMenusActiveError

cms/snippets/models/menu_builder/menu_link.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,39 @@ def get_prep_value(self, value: StructValue) -> dict[str, str | int]:
5656
prep_value["html_url"] = page.full_url
5757

5858
return prep_value
59+
60+
61+
class SimpleMenuLink(blocks.StructBlock):
62+
title = blocks.TextBlock(
63+
required=True,
64+
help_text=help_texts.MENU_LINK_HELP_TEXT,
65+
)
66+
page = blocks.PageChooserBlock(
67+
"wagtailcore.Page",
68+
related_name="+",
69+
on_delete=models.CASCADE,
70+
)
71+
72+
class Meta:
73+
icon = "link"
74+
75+
def get_prep_value(self, value: StructValue) -> dict[str, str | int]:
76+
"""Adds the `html_url` of each page to the returned value
77+
78+
Args:
79+
`value`: The inbound enriched `StructValue`
80+
containing the values associated with
81+
this `SimpleMenuLink` object
82+
83+
Returns:
84+
Dict containing the keys as dictated by the
85+
`SimpleMenuLink`. With the addition of the injected
86+
`html_url` value for the selected page.
87+
88+
"""
89+
prep_value: dict[str, str | int] = super().get_prep_value(value=value)
90+
page: Page = value["page"]
91+
page: type[UKHSAPage] = page.specific
92+
prep_value["html_url"] = page.full_url
93+
94+
return prep_value

cms/snippets/serializers/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
InternalButtonSerializer,
55
)
66
from .global_banner import (
7-
GlobalBannerSerializer,
87
GlobalBannerResponseSerializer,
8+
GlobalBannerSerializer,
9+
)
10+
from .menu import (
11+
MenuResponseSerializer,
12+
MenuSerializer,
13+
SimpleMenuResponseSerializer,
14+
SimpleMenuSerializer,
915
)
10-
from .menu import MenuSerializer, MenuResponseSerializer

0 commit comments

Comments
 (0)