Skip to content

Commit ec36d7b

Browse files
committed
Merge branch 'release/9.1.0'
2 parents e2816c5 + 815e849 commit ec36d7b

File tree

9 files changed

+221
-39
lines changed

9 files changed

+221
-39
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,14 @@ jobs:
77
runs-on: ubuntu-latest
88
strategy:
99
matrix:
10-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
11-
wagtail-version: ["6.3", "7.0", "7.1"]
10+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
11+
wagtail-version: ["6.3", "7.0", "7.1", "7.2"]
1212
django-version: ["4.2", "5.1", "5.2"]
1313
exclude:
1414
- python-version: "3.13"
1515
django-version: "4.2"
1616
- python-version: "3.13"
1717
django-version: "5.0"
18-
- python-version: "3.9"
19-
django-version: "5.1"
20-
- python-version: "3.9"
21-
django-version: "5.2"
2218
services:
2319
postgres:
2420
image: postgis/postgis:15-3.4

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,6 @@ tests/media/*
6060

6161
# Local env
6262
.env.local
63+
64+
# Claude
65+
CLAUDE.local.md

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@
66
### Fixed
77
### Removed
88

9+
## [9.1.0] - 2025.11.09
10+
### Added
11+
- Add support for Wagtail 7.2 (@marteinn)
12+
- Add support for Python 3.14 (@marteinn)
13+
14+
### Fixed
15+
- Fix double initialization bug in Wagtail 7.1+ causing streamfield map blocks not to read existing values ending up only showing default (@marteinn)
16+
- Ignore CLAUDE.local.md config (@marteinn)
17+
18+
### Removed
19+
- Drop support for EOL Python 3.9 (@marteinn)
20+
921
## [9.0.0] - 2025.08.14
1022
### Added
1123
- Add support for Wagtail 7.1 (@marteinn)

docs/getting-started-with-google-maps.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
### Requirements
44

5-
- Python 3.9+
5+
- Python 3.10+
66
- Wagtail 6.3+ and Django 4.2+
77
- A Google account
88

docs/getting-started-with-leaflet.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
### Requirements
44

5-
- Python 3.9+
5+
- Python 3.10+
66
- Wagtail 6.3+ and Django 4.2+
77
- Access to a tile provider for Leaflet, this library includes built in support for Open Street Map
88

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@
5252
"Operating System :: OS Independent",
5353
"Programming Language :: Python",
5454
"Programming Language :: Python :: 3",
55-
"Programming Language :: Python :: 3.9",
5655
"Programming Language :: Python :: 3.10",
5756
"Programming Language :: Python :: 3.11",
5857
"Programming Language :: Python :: 3.12",
5958
"Programming Language :: Python :: 3.13",
59+
"Programming Language :: Python :: 3.14",
6060
"Topic :: Utilities",
6161
],
6262
extras_require={"test": test_extras},

tests/test_widgets.py

Lines changed: 169 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1+
import unittest
2+
13
from django.test import TestCase
4+
from wagtail import VERSION as WAGTAIL_VERSION
25

36
from wagtailgeowidget import app_settings, geocoders
4-
from wagtailgeowidget.widgets import GeocoderField, GoogleMapsField, LeafletField
7+
from wagtailgeowidget.widgets import (
8+
GeocoderField,
9+
GoogleMapsField,
10+
GoogleMapsFieldAdapter,
11+
LeafletField,
12+
LeafletFieldAdapter,
13+
)
514

615

716
class GoogleMapsFieldTestCase(TestCase):
8-
def test_google_maps_field_contains_constuct(self):
17+
def test_google_maps_field_contains_construct(self):
918
widget = GoogleMapsField()
1019
html = widget.render(
1120
"field",
@@ -20,33 +29,99 @@ def test_google_maps_field_contains_constuct(self):
2029
html,
2130
)
2231

32+
def test_streamfield_widget_uses_empty_id_prefix(self):
33+
"""Test that StreamField widgets use empty id_prefix."""
2334

24-
class LeafletFieldTestCase(TestCase):
25-
def test_leaflet_field_contains_constuct(self):
26-
widget = LeafletField()
35+
widget = GoogleMapsField(srid=4326, id_prefix="")
36+
37+
self.assertEqual(widget.id_prefix, "")
38+
39+
def test_fieldpanel_widget_includes_stimulus_attributes(self):
40+
"""Test that FieldPanel widgets (id_prefix='id_') include Stimulus controller attributes."""
41+
42+
widget = GoogleMapsField(srid=4326, id_prefix="id_")
2743
html = widget.render(
2844
"field",
29-
"",
45+
"SRID=4326;POINT(18.0686 59.3293)",
3046
{
31-
"id": "X",
47+
"id": "test-field",
3248
},
3349
)
3450

35-
self.assertIn(
36-
'<input type="hidden" name="field" id="X" data-controller="leaflet-field"',
37-
html,
51+
self.assertIn('data-controller="google-maps-field"', html)
52+
self.assertIn("data-google-maps-field-options-value=", html)
53+
54+
def test_streamfield_widget_excludes_stimulus_attributes(self):
55+
"""Test that StreamField widgets (id_prefix='') exclude Stimulus controller attributes."""
56+
widget = GoogleMapsField(srid=4326, id_prefix="")
57+
58+
html = widget.render(
59+
"field",
60+
"SRID=4326;POINT(18.0686 59.3293)",
61+
{
62+
"id": "test-field",
63+
},
3864
)
3965

40-
def test_leaflet_field_js_init_contains_construct(self):
41-
widget = LeafletField()
66+
self.assertNotIn('data-controller="google-maps-field"', html)
67+
self.assertNotIn("data-google-maps-field-options-value=", html)
68+
69+
@unittest.skipIf(WAGTAIL_VERSION < (7, 1), "Test only applicable for Wagtail 7.1+")
70+
def test_telepath_adapter_js_args_structure(self):
71+
"""Test that the adapter returns correct js_args structure for Telepath."""
72+
73+
widget = GoogleMapsField(
74+
srid=4326,
75+
address_field="address",
76+
zoom_field="zoom",
77+
)
78+
adapter = GoogleMapsFieldAdapter()
79+
80+
result = adapter.js_args(widget)
81+
82+
self.assertEqual(len(result), 2)
83+
84+
self.assertIsInstance(result[0], str)
85+
self.assertIn('<input type="hidden"', result[0])
86+
87+
self.assertIsInstance(result[1], dict)
88+
options = result[1]
89+
90+
self.assertIn("srid", options)
91+
self.assertIn("addressField", options)
92+
self.assertIn("zoomField", options)
93+
self.assertIn("defaultLocation", options)
94+
self.assertIn("zoom", options)
95+
self.assertIn("mapId", options)
4296

97+
self.assertEqual(options["srid"], 4326)
98+
self.assertEqual(options["addressField"], "address")
99+
self.assertEqual(options["zoomField"], "zoom")
100+
101+
def test_telepath_adapter_streamfield_excludes_stimulus_attributes(self):
102+
"""Test that HTML by adapter for StreamField widget has no Stimulus attributes."""
103+
104+
widget = GoogleMapsField(srid=4326, id_prefix="")
105+
adapter = GoogleMapsFieldAdapter()
106+
107+
result = adapter.js_args(widget)
108+
html = result[0]
109+
110+
self.assertNotIn("data-controller=", html)
111+
self.assertNotIn("data-google-maps-field-options-value=", html)
112+
113+
114+
class LeafletFieldTestCase(TestCase):
115+
def test_leaflet_field_contains_construct(self):
116+
widget = LeafletField()
43117
html = widget.render(
44118
"field",
45119
"",
46120
{
47121
"id": "X",
48122
},
49123
)
124+
50125
self.assertIn(
51126
'<input type="hidden" name="field" id="X" data-controller="leaflet-field"',
52127
html,
@@ -67,13 +142,94 @@ def test_value_are_parsed_properly(self):
67142
self.assertIn(escape('"lat": "13.0"'), html)
68143
self.assertIn(escape('"lng": "12.0"'), html)
69144

145+
def test_streamfield_widget_uses_empty_id_prefix(self):
146+
"""Test that StreamField widgets use empty id_prefix."""
147+
148+
widget = LeafletField(srid=4326, id_prefix="")
149+
150+
self.assertEqual(widget.id_prefix, "")
151+
152+
def test_fieldpanel_widget_includes_stimulus_attributes(self):
153+
"""Test that FieldPanel widgets (id_prefix='id_') include Stimulus controller attributes."""
154+
155+
widget = LeafletField(srid=4326, id_prefix="id_")
156+
html = widget.render(
157+
"field",
158+
"SRID=4326;POINT(18.0686 59.3293)",
159+
{
160+
"id": "test-field",
161+
},
162+
)
163+
164+
self.assertIn('data-controller="leaflet-field"', html)
165+
self.assertIn("data-leaflet-field-options-value=", html)
166+
167+
def test_streamfield_widget_excludes_stimulus_attributes(self):
168+
"""Test that StreamField widgets (id_prefix='') exclude Stimulus controller attributes."""
169+
170+
widget = LeafletField(srid=4326, id_prefix="")
171+
172+
html = widget.render(
173+
"field",
174+
"SRID=4326;POINT(18.0686 59.3293)",
175+
{
176+
"id": "test-field",
177+
},
178+
)
179+
180+
self.assertNotIn('data-controller="leaflet-field"', html)
181+
self.assertNotIn("data-leaflet-field-options-value=", html)
182+
183+
@unittest.skipIf(WAGTAIL_VERSION < (7, 1), "Test only applicable for Wagtail 7.1+")
184+
def test_telepath_adapter_js_args_structure(self):
185+
"""Test that the adapter returns correct js_args structure for Telepath."""
186+
187+
widget = LeafletField(
188+
srid=4326,
189+
address_field="address",
190+
zoom_field="zoom",
191+
)
192+
adapter = LeafletFieldAdapter()
193+
194+
result = adapter.js_args(widget)
195+
196+
self.assertEqual(len(result), 2)
197+
198+
self.assertIsInstance(result[0], str)
199+
self.assertIn('<input type="hidden"', result[0])
200+
201+
self.assertIsInstance(result[1], dict)
202+
options = result[1]
203+
204+
self.assertIn("srid", options)
205+
self.assertIn("addressField", options)
206+
self.assertIn("zoomField", options)
207+
self.assertIn("defaultLocation", options)
208+
self.assertIn("zoom", options)
209+
210+
self.assertEqual(options["srid"], 4326)
211+
self.assertEqual(options["addressField"], "address")
212+
self.assertEqual(options["zoomField"], "zoom")
213+
214+
def test_telepath_adapter_streamfield_excludes_stimulus_attributes(self):
215+
"""Test that HTML by adapter for StreamField widget has no Stimulus attributes."""
216+
217+
widget = LeafletField(srid=4326, id_prefix="")
218+
adapter = LeafletFieldAdapter()
219+
220+
result = adapter.js_args(widget)
221+
html = result[0]
222+
223+
self.assertNotIn("data-controller=", html)
224+
self.assertNotIn("data-leaflet-field-options-value=", html)
225+
70226

71227
class GeocoderFieldTestCase(TestCase):
72228
def setUp(self):
73229
app_settings.MAPBOX_ACCESS_TOKEN = None
74230
app_settings.MAPBOX_LANGUAGE = "en"
75231

76-
def test_geocoder_field_contains_constuct(self):
232+
def test_geocoder_field_contains_construct(self):
77233
widget = GeocoderField()
78234
html = widget.render(
79235
"field",

wagtailgeowidget/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
"""
88

99
__title__ = "wagtailgeowidget"
10-
__version__ = "9.0.0"
11-
__build__ = 900
10+
__version__ = "9.1.0"
11+
__build__ = 901
1212
__author__ = "Martin Sandström"
1313
__license__ = "MIT"
1414
__copyright__ = "Copyright 2015-Present Fröjd Interactive"

wagtailgeowidget/widgets.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@ def __init__(self, *args, **kwargs):
6666
super().__init__(*args, **kwargs)
6767

6868
def build_attrs(self, *args, **kwargs):
69+
attrs = super().build_attrs(*args, **kwargs)
70+
71+
# Don't add Stimulus controller attributes if this widget is being used
72+
# in a StreamField/StructBlock context (id_prefix=""). In that context,
73+
# Telepath handles initialization. For FieldPanel (id_prefix="id_"),
74+
# Stimulus is still needed even though Telepath is used for serialization.
75+
if self.id_prefix == "":
76+
return attrs
77+
6978
data = {
7079
"defaultLocation": GEO_WIDGET_DEFAULT_LOCATION,
7180
"addressField": self.address_field,
@@ -91,7 +100,6 @@ def build_attrs(self, *args, **kwargs):
91100
"lng": self.value_data.x,
92101
}
93102

94-
attrs = super().build_attrs(*args, **kwargs)
95103
attrs["data-controller"] = "google-maps-field"
96104
attrs["data-google-maps-field-options-value"] = json.dumps(data)
97105
return attrs
@@ -249,6 +257,15 @@ def __init__(self, *args, **kwargs):
249257
super().__init__(*args, **kwargs)
250258

251259
def build_attrs(self, *args, **kwargs):
260+
attrs = super().build_attrs(*args, **kwargs)
261+
262+
# Don't add Stimulus controller attributes if this widget is being used
263+
# in a StreamField/StructBlock context (id_prefix=""). In that context,
264+
# Telepath handles initialization. For FieldPanel (id_prefix="id_"),
265+
# Stimulus is still needed even though Telepath is used for serialization.
266+
if self.id_prefix == "":
267+
return attrs
268+
252269
data = {
253270
"defaultLocation": GEO_WIDGET_DEFAULT_LOCATION,
254271
"addressField": self.address_field,
@@ -275,7 +292,6 @@ def build_attrs(self, *args, **kwargs):
275292
"lng": self.value_data.x,
276293
}
277294

278-
attrs = super().build_attrs(*args, **kwargs)
279295
attrs["data-controller"] = "leaflet-field"
280296
attrs["data-leaflet-field-options-value"] = json.dumps(data)
281297
return attrs
@@ -333,19 +349,18 @@ class GoogleMapsFieldAdapter(WidgetAdapter):
333349
def js_args(self, widget):
334350
args = super().js_args(widget)
335351

336-
return [
337-
*args,
338-
{
339-
"addressField": widget.address_field,
340-
"zoomField": widget.zoom_field,
341-
"defaultLocation": GEO_WIDGET_DEFAULT_LOCATION,
342-
"srid": widget.srid,
343-
"zoom": widget.zoom,
344-
"showEmptyLocation": GEO_WIDGET_EMPTY_LOCATION,
345-
"translations": translations,
346-
"mapId": widget.map_id,
347-
},
348-
]
352+
options = {
353+
"addressField": widget.address_field,
354+
"zoomField": widget.zoom_field,
355+
"defaultLocation": GEO_WIDGET_DEFAULT_LOCATION,
356+
"srid": widget.srid,
357+
"zoom": widget.zoom,
358+
"showEmptyLocation": GEO_WIDGET_EMPTY_LOCATION,
359+
"translations": translations,
360+
"mapId": widget.map_id,
361+
}
362+
363+
return [*args, options]
349364

350365
class Media:
351366
js = ["wagtailgeowidget/js/google-maps-field-telepath.js"]

0 commit comments

Comments
 (0)