Skip to content

Commit a9442b9

Browse files
Merge branch 'master' into feature/709-flag-invalid-prs
2 parents e719d10 + 6496fa3 commit a9442b9

9 files changed

Lines changed: 94 additions & 25 deletions

File tree

docs/developer/admin-theme.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ customize admin theme.
122122
to use the ``STATICFILES_DIR`` setting in ``settings.py``.
123123

124124
You can learn more in the `Django documentation
125-
<https://docs.djangoproject.com/en/4.2/ref/settings/#std:setting-STATICFILES_DIRS>`_.
125+
<https://docs.djangoproject.com/en/5.2/ref/settings/#std:setting-STATICFILES_DIRS>`_.
126126

127127
Extend Admin Theme Programmatically
128128
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

docs/developer/admin-utilities.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ by modifying ``InputFilter`` as following:
157157
158158
To know about other lookups that can be used please check `Django Lookup
159159
API Reference
160-
<https://docs.djangoproject.com/en/4.2/ref/models/lookups/#django.db.models.Lookup>`__
160+
<https://docs.djangoproject.com/en/5.2/ref/models/lookups/#django.db.models.Lookup>`__
161161

162162
``openwisp_utils.admin_theme.filters.SimpleInputFilter``
163163
--------------------------------------------------------

docs/developer/custom-fields.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ openwisp modules.
2020
----------------------------------------------------
2121

2222
This field extends Django's `BooleanField
23-
<https://docs.djangoproject.com/en/4.2/ref/models/fields/#booleanfield>`_
23+
<https://docs.djangoproject.com/en/5.2/ref/models/fields/#booleanfield>`_
2424
and provides additional functionality for handling choices with a fallback
2525
value.
2626

@@ -45,7 +45,7 @@ between enabled and disabled options.
4545
-------------------------------------------------
4646

4747
This field extends Django's `CharField
48-
<https://docs.djangoproject.com/en/4.2/ref/models/fields/#charfield>`_ and
48+
<https://docs.djangoproject.com/en/5.2/ref/models/fields/#charfield>`_ and
4949
provides additional functionality for handling choices with a fallback
5050
value.
5151

@@ -73,7 +73,7 @@ value.
7373
-------------------------------------------
7474

7575
This field extends Django's `CharField
76-
<https://docs.djangoproject.com/en/4.2/ref/models/fields/#charfield>`_ and
76+
<https://docs.djangoproject.com/en/5.2/ref/models/fields/#charfield>`_ and
7777
provides additional functionality for handling text fields with a fallback
7878
value.
7979

@@ -96,7 +96,7 @@ value.
9696
------------------------------------------
9797

9898
This field extends Django's `URLField
99-
<https://docs.djangoproject.com/en/4.2/ref/models/fields/#urlfield>`_ and
99+
<https://docs.djangoproject.com/en/5.2/ref/models/fields/#urlfield>`_ and
100100
provides additional functionality for handling URL fields with a fallback
101101
value.
102102

@@ -119,7 +119,7 @@ value.
119119
-------------------------------------------
120120

121121
This extends Django's `TextField
122-
<https://docs.djangoproject.com/en/4.2/ref/models/fields/#django.db.models.TextField>`_
122+
<https://docs.djangoproject.com/en/5.2/ref/models/fields/#django.db.models.TextField>`_
123123
and provides additional functionality for handling text fields with a
124124
fallback value.
125125

@@ -142,7 +142,7 @@ fallback value.
142142
------------------------------------------------------
143143

144144
This extends Django's `PositiveIntegerField
145-
<https://docs.djangoproject.com/en/4.2/ref/models/fields/#positiveintegerfield>`_
145+
<https://docs.djangoproject.com/en/5.2/ref/models/fields/#positiveintegerfield>`_
146146
and provides additional functionality for handling positive integer fields
147147
with a fallback value.
148148

@@ -164,7 +164,7 @@ with a fallback value.
164164
----------------------------------------------
165165

166166
This extends Django's `DecimalField
167-
<https://docs.djangoproject.com/en/4.2/ref/models/fields/#decimalfield>`_
167+
<https://docs.djangoproject.com/en/5.2/ref/models/fields/#decimalfield>`_
168168
and provides additional functionality for handling decimal fields with a
169169
fallback value.
170170

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

docs/developer/test-utilities.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Usage example as a context-manager:
3737
:align: center
3838

3939
This class extends the `default test runner provided by Django
40-
<https://docs.djangoproject.com/en/4.2/ref/settings/#std:setting-TEST_RUNNER>`_
40+
<https://docs.djangoproject.com/en/5.2/ref/settings/#std:setting-TEST_RUNNER>`_
4141
and logs the time spent by each test, making it easier to spot slow tests
4242
by highlighting time taken by it in yellow (time shall be highlighted in
4343
red if it crosses the second threshold).
@@ -152,7 +152,7 @@ Example usage:
152152
-----------------------------------------------------
153153

154154
This mixin overrides the `assertNumQueries
155-
<https://docs.djangoproject.com/en/4.2/topics/testing/tools/#django.test.TransactionTestCase.assertNumQueries>`_
155+
<https://docs.djangoproject.com/en/5.2/topics/testing/tools/#django.test.TransactionTestCase.assertNumQueries>`_
156156
assertion from the django test case to run in a ``subTest`` so that the
157157
query check does not block the whole test if it fails.
158158

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\

openwisp_utils/tests/selenium.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -367,26 +367,32 @@ def login(self, username=None, password=None, driver=None):
367367
driver.get(f"{self.live_server_url}/admin/login/")
368368
self._wait_until_page_ready(driver=driver)
369369
if "admin/login" in driver.current_url:
370-
driver.find_element(by=By.NAME, value="username").send_keys(username)
371-
driver.find_element(by=By.NAME, value="password").send_keys(password)
372-
driver.find_element(by=By.XPATH, value='//input[@type="submit"]').click()
370+
self.find_element(by=By.NAME, value="username", driver=driver).send_keys(
371+
username
372+
)
373+
self.find_element(by=By.NAME, value="password", driver=driver).send_keys(
374+
password
375+
)
376+
self.find_element(
377+
by=By.XPATH, value='//input[@type="submit"]', driver=driver
378+
).click()
373379
self._wait_until_page_ready(driver=driver)
374380

375381
def logout(self, driver=None):
376382
driver = driver or self.web_driver
377-
self.web_driver.find_element(By.CSS_SELECTOR, ".account-button").click()
378-
self.web_driver.find_element(By.CSS_SELECTOR, "#logout-form button").click()
383+
self.find_element(By.CSS_SELECTOR, ".account-button", driver=driver).click()
384+
self.find_element(By.CSS_SELECTOR, "#logout-form button", driver=driver).click()
379385

380386
def find_element(self, by, value, timeout=2, driver=None, wait_for="visibility"):
381387
driver = driver or self.web_driver
382388
method = f"wait_for_{wait_for}"
383-
getattr(self, method)(by, value, timeout)
389+
getattr(self, method)(by, value, timeout, driver=driver)
384390
return driver.find_element(by=by, value=value)
385391

386392
def find_elements(self, by, value, timeout=2, driver=None, wait_for="visibility"):
387393
driver = driver or self.web_driver
388394
method = f"wait_for_{wait_for}"
389-
getattr(self, method)(by, value, timeout)
395+
getattr(self, method)(by, value, timeout, driver=driver)
390396
return driver.find_elements(by=by, value=value)
391397

392398
def wait_for_visibility(self, by, value, timeout=2, driver=None):

requirements-test.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# For testing Dependency loaders
2-
openwisp_controller @ https://github.com/openwisp/openwisp-controller/tarball/master
2+
openwisp_controller @ https://github.com/openwisp/openwisp-controller/archive/refs/heads/master.tar.gz
33
freezegun
44
pytest
55
pytest-mock

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)