Skip to content

Commit 6496fa3

Browse files
author
Saksham Jain
authored
[change] Made menu item registration idempotent #641
Previously, calling `register_menu_group` or `register_menu_subitem` twice with the same configuration at the same position would always raise an `ImproperlyConfigured` exception. This behavior prevented re-registration, even when the same application was re-initializing its menu items during testing. This change modifies these functions to check if the configuration being registered is identical to the one already present at that position. If the configurations match, the function now logs an informational message and skips the re-registration instead of raising an error. This makes the menu item registration process idempotent, improving testability and handling of repeated app initializations. Closes #641
1 parent 3592043 commit 6496fa3

3 files changed

Lines changed: 69 additions & 6 deletions

File tree

docs/developer/navigation-menu.rst

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,12 @@ Code example:
9797
position=3, config={"label": _("My Link"), "url": "https://link.com"}
9898
)
9999
100-
An ``ImproperlyConfigured`` exception is raised if a menu element is
101-
already registered at the same position.
100+
If the same ``config`` is registered at the same position more than once
101+
(e.g. because ``AppConfig.ready()`` is called again during testing), the
102+
call is a no-op and a log message is emitted at ``INFO`` level.
103+
104+
An ``ImproperlyConfigured`` exception is raised if a *different* menu
105+
element is already registered at the same position.
102106

103107
An ``ImproperlyConfigured`` exception is raised if the supplied
104108
configuration does not match with the different types of possible
@@ -278,8 +282,10 @@ Code example:
278282
An ``ImproperlyConfigured`` exception is raised if the group is not
279283
already registered at ``group_position``.
280284

281-
An ``ImproperlyConfigured`` exception is raised if the group already has
282-
an item registered at ``item_position``.
285+
If the same ``config`` is registered at the same ``item_position`` more
286+
than once, the call is a no-op and a log message is emitted at ``INFO``
287+
level. An ``ImproperlyConfigured`` exception is raised if a *different*
288+
item is already registered at ``item_position``.
283289

284290
It is only possible to register links to specific models or custom URL. An
285291
``ImproperlyConfigured`` exception is raised if the configuration of group

openwisp_utils/admin_theme/menu.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import logging
2+
13
from django.apps import registry
24
from django.core.exceptions import ImproperlyConfigured
35
from django.urls import reverse
46
from django.urls.exceptions import NoReverseMatch
57

68
from ..utils import SortedOrderedDict
79

10+
logger = logging.getLogger(__name__)
11+
812
MENU = SortedOrderedDict()
913

1014

@@ -19,6 +23,7 @@ def __init__(self, config):
1923
raise ImproperlyConfigured(
2024
f'"config" should be a type of "dict". Error for config- {config}'
2125
)
26+
self.config = config
2227

2328
def get_context(self, request=None):
2429
return self.create_context(request)
@@ -60,7 +65,6 @@ def __init__(self, config):
6065
self.model = model
6166
self.set_label(config)
6267
self.icon = config.get("icon")
63-
self.config = config
6468

6569
def set_label(self, config=None):
6670
if config.get("label"):
@@ -185,6 +189,12 @@ def register_menu_group(position, config):
185189
if not isinstance(config, dict):
186190
raise ImproperlyConfigured('config should be a type of "dict"')
187191
if position in MENU:
192+
if MENU[position].config == config:
193+
logger.info(
194+
f"A group/link with config {config} is already registered at"
195+
f' position "{position}", skipping re-registration.'
196+
)
197+
return
188198
item_description = "link"
189199
if isinstance(MENU[position], MenuGroup):
190200
item_description = "group"
@@ -244,6 +254,13 @@ def register_menu_subitem(group_position, item_position, config):
244254
f'Invalid config "{config}" provided for sub group item'
245255
)
246256
if item_position in group.items:
257+
if group.items[item_position].config == config:
258+
logger.info(
259+
f"A group item with config {config} is already registered at"
260+
f' position "{item_position}" in the group at position'
261+
f' "{group_position}", skipping re-registration.'
262+
)
263+
return
247264
name = group.items[item_position]
248265
raise ImproperlyConfigured(
249266
f'A group item with config {config} is being registered at position\

tests/test_project/tests/test_menu.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,32 @@ def test_register_menu_groups(self):
8888
with self.assertRaises(ImproperlyConfigured):
8989
register_menu_group(position=1, config=model_link_config)
9090

91+
with self.subTest(
92+
"Registering identical config at an occupied position is idempotent"
93+
):
94+
with self.assertLogs(
95+
"openwisp_utils.admin_theme.menu", level="INFO"
96+
) as captured:
97+
register_menu_group(position=1, config=menu_link_config)
98+
self.assertIn("already registered", captured.output[0])
99+
self.assertIsInstance(MENU[1], MenuLink)
100+
101+
with self.subTest("Re-registering identical ModelLink config is idempotent"):
102+
with self.assertLogs(
103+
"openwisp_utils.admin_theme.menu", level="INFO"
104+
) as captured:
105+
register_menu_group(position=2, config=model_link_config)
106+
self.assertIn("already registered", captured.output[0])
107+
self.assertIsInstance(MENU[2], ModelLink)
108+
109+
with self.subTest("Re-registering identical MenuGroup config is idempotent"):
110+
with self.assertLogs(
111+
"openwisp_utils.admin_theme.menu", level="INFO"
112+
) as captured:
113+
register_menu_group(position=3, config=menu_group_config)
114+
self.assertIn("already registered", captured.output[0])
115+
self.assertIsInstance(MENU[3], MenuGroup)
116+
91117
with self.subTest("Registering with invalid position"):
92118
with self.assertRaises(ImproperlyConfigured):
93119
register_menu_group(
@@ -143,7 +169,9 @@ def test_register_menu_subitem(self):
143169
register_menu_subitem(group_position=100, item_position=1, config=[])
144170
with self.assertRaises(ImproperlyConfigured):
145171
register_menu_subitem(
146-
group_position=100, item_position=1, config=config
172+
group_position=100,
173+
item_position=1,
174+
config=self._get_menu_link_config(label="a different label"),
147175
)
148176
with self.assertRaises(ImproperlyConfigured):
149177
register_menu_subitem(
@@ -156,6 +184,18 @@ def test_register_menu_subitem(self):
156184
config=self._get_menu_group_config(),
157185
)
158186

187+
with self.subTest(
188+
"Registering identical config at an occupied item position is idempotent"
189+
):
190+
with self.assertLogs(
191+
"openwisp_utils.admin_theme.menu", level="INFO"
192+
) as captured:
193+
register_menu_subitem(
194+
group_position=100, item_position=1, config=config
195+
)
196+
self.assertIn("already registered", captured.output[0])
197+
self.assertIsInstance(MENU[100].items[1], MenuLink)
198+
159199
with self.subTest("Test menu subitem with valid data"):
160200
model_link_config = self._get_model_link_config()
161201
register_menu_subitem(group_position=100, item_position=3, config=config)

0 commit comments

Comments
 (0)