Skip to content

Commit ac64cc2

Browse files
committed
fix wiring
1 parent 21b646a commit ac64cc2

6 files changed

Lines changed: 234 additions & 4 deletions

File tree

‎frontend/src/components/Map/map.config.js‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
// The view the frontend opened on before it became configurable: the centre of Poland,
2-
// zoomed out far enough to hold the country. A deliberate second copy of
3-
// goodmap/initial_view.py's defaults - this module cannot reach Python, and editing both is
4-
// the price of that.
1+
// Where the map opens when the page set no window.INITIAL_VIEW: the centre of Poland,
2+
// zoomed out far enough to hold the country. goodmap/initial_view.py resolves these same
3+
// defaults server-side and normally sends a complete view, so these values are reached only
4+
// by a bundle running outside map.html. Nothing checks that the two agree - keep them in
5+
// step by hand.
56
const DEFAULT_VIEW = {
67
center: [51.917, 19.013],
78
zoom: 7,

‎goodmap/db.py‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,70 @@ def get_marker_styles(db):
626626
return globals()[f"{db.module_name}_get_marker_styles"]
627627

628628

629+
# ------------------------------------------------
630+
# get_initial_view
631+
632+
633+
def google_json_db_get_initial_view(self) -> dict[str, Any]:
634+
"""
635+
Retrieve the map's opening view from Google Cloud Storage JSON blob.
636+
637+
Returns:
638+
dict: Raw ``initial_view`` config (center, zoom, max_zoom). Returns empty dict if
639+
not found - declaring a view is optional.
640+
"""
641+
return self.data.get("map", {}).get("initial_view", {})
642+
643+
644+
def json_file_db_get_initial_view(self) -> dict[str, Any]:
645+
"""
646+
Retrieve the map's opening view from JSON file database.
647+
648+
Returns:
649+
dict: Raw ``initial_view`` config (center, zoom, max_zoom). Returns empty dict if
650+
not found - declaring a view is optional.
651+
"""
652+
return self.data.get("map", {}).get("initial_view", {})
653+
654+
655+
def json_db_get_initial_view(self) -> dict[str, Any]:
656+
"""
657+
Retrieve the map's opening view from in-memory JSON database.
658+
659+
Returns:
660+
dict: Raw ``initial_view`` config (center, zoom, max_zoom). Returns empty dict if
661+
not found - declaring a view is optional.
662+
"""
663+
return self.data.get("initial_view", {})
664+
665+
666+
def mongodb_db_get_initial_view(self) -> dict[str, Any]:
667+
"""
668+
Retrieve the map's opening view from MongoDB.
669+
670+
Returns:
671+
dict: Raw ``initial_view`` config (center, zoom, max_zoom). Returns empty dict if
672+
the config document is missing or does not declare one.
673+
"""
674+
config_doc = self.db.config.find_one({"_id": "map_config"})
675+
if config_doc:
676+
return config_doc.get("initial_view", {})
677+
return {}
678+
679+
680+
def get_initial_view(db):
681+
"""
682+
Get the appropriate get_initial_view function for the given database backend.
683+
684+
Args:
685+
db: Database instance (must have module_name attribute).
686+
687+
Returns:
688+
callable: Backend-specific get_initial_view function.
689+
"""
690+
return globals()[f"{db.module_name}_get_initial_view"]
691+
692+
629693
# ------------------------------------------------
630694
# get_categories
631695

@@ -1842,6 +1906,7 @@ def extend_db_with_goodmap_queries(db, location_model):
18421906
db.extend("get_visible_data", get_visible_data(db))
18431907
db.extend("get_meta_data", get_meta_data(db))
18441908
db.extend("get_marker_styles", get_marker_styles(db))
1909+
db.extend("get_initial_view", get_initial_view(db))
18451910
db.extend("get_locations", get_locations(db, location_model))
18461911
db.extend("get_locations_paginated", get_locations_paginated(db, location_model))
18471912
db.extend("get_location", get_location(db, location_model))

‎goodmap/goodmap.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222
from goodmap.db import (
2323
extend_db_with_goodmap_queries,
2424
get_category_data,
25+
get_initial_view,
2526
get_location_obligatory_fields,
2627
get_marker_styles,
2728
)
2829
from goodmap.feature_flags import EnableAdminPanel
30+
from goodmap.initial_view import resolve_initial_view
2931
from goodmap.marker_styles import resolve_marker_styles
3032
from goodmap.plugin import CAPABILITY_BASES, GoodmapPluginBase
3133

@@ -193,6 +195,11 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
193195
raw_marker_styles = get_marker_styles(app.db)(app.db)
194196
marker_styles = resolve_marker_styles(raw_marker_styles)
195197

198+
# Resolved once at startup rather than per request: the view cannot change without a
199+
# restart, and resolving here is what makes a bad one fail the boot instead of every
200+
# page load (see goodmap.initial_view).
201+
initial_view = resolve_initial_view(get_initial_view(app.db)(app.db))
202+
196203
location_model = create_location_model(location_obligatory_fields, categories)
197204
app.db = extend_db_with_goodmap_queries(app.db, location_model)
198205

@@ -292,6 +299,7 @@ def index():
292299
goodmap_frontend_lib_url=config.goodmap_frontend_lib_url,
293300
plugin_manifest=plugin_manifest,
294301
marker_styles=marker_styles,
302+
initial_view=initial_view,
295303
)
296304

297305
@goodmap.route("/goodmap-admin")

‎goodmap/templates/map.html‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
window.FEATURE_FLAGS = {{ feature_flags | tojson }};
122122
window.PLUGIN_MANIFEST = {{ plugin_manifest | tojson }};
123123
window.MARKER_STYLES = {{ marker_styles | tojson }};
124+
window.INITIAL_VIEW = {{ initial_view | tojson }};
124125
</script>
125126
<script src="{{ goodmap_frontend_lib_url }}" crossorigin="anonymous"></script>
126127
{% endblock %}

‎tests/unit_tests/test_db.py‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
google_json_db_get_categories,
2525
google_json_db_get_category_data,
2626
google_json_db_get_data,
27+
google_json_db_get_initial_view,
2728
google_json_db_get_location_obligatory_fields,
2829
google_json_db_get_locations_paginated,
2930
google_json_db_get_marker_styles,
@@ -38,6 +39,7 @@
3839
json_db_get_categories,
3940
json_db_get_category_data,
4041
json_db_get_data,
42+
json_db_get_initial_view,
4143
json_db_get_location_obligatory_fields,
4244
json_db_get_locations,
4345
json_db_get_report,
@@ -57,6 +59,7 @@
5759
json_file_db_get_categories,
5860
json_file_db_get_category_data,
5961
json_file_db_get_data,
62+
json_file_db_get_initial_view,
6063
json_file_db_get_location_obligatory_fields,
6164
json_file_db_get_locations,
6265
json_file_db_get_locations_paginated,
@@ -81,6 +84,7 @@
8184
mongodb_db_get_categories,
8285
mongodb_db_get_category_data,
8386
mongodb_db_get_data,
87+
mongodb_db_get_initial_view,
8488
mongodb_db_get_location,
8589
mongodb_db_get_location_obligatory_fields,
8690
mongodb_db_get_locations,
@@ -340,6 +344,91 @@ def test_json_file_db_get_marker_styles_empty():
340344
assert result == {}
341345

342346

347+
# ------------------------------------------------
348+
# get_initial_view
349+
#
350+
# Every backend returns the raw config untouched; resolve_initial_view fills the blanks in,
351+
# so "declared nothing" has to reach it as {} rather than as a backend's own guess at a view.
352+
353+
354+
@mock.patch(
355+
"builtins.open",
356+
mock.mock_open(
357+
read_data=json.dumps({"map": {"initial_view": {"center": [53.37, 22.89], "zoom": 8}}})
358+
),
359+
)
360+
def test_json_file_db_get_initial_view():
361+
db = JsonFile("/fake/path/data.json")
362+
assert json_file_db_get_initial_view(db) == {"center": [53.37, 22.89], "zoom": 8}
363+
364+
365+
@mock.patch("builtins.open", mock.mock_open(read_data=json.dumps({"map": {}})))
366+
def test_json_file_db_get_initial_view_empty():
367+
db = JsonFile("/fake/path/data.json")
368+
assert json_file_db_get_initial_view(db) == {}
369+
370+
371+
def test_json_db_get_initial_view():
372+
db = in_memory_json_db({"initial_view": {"center": [10.0, 20.0], "zoom": 3, "max_zoom": 12}})
373+
assert json_db_get_initial_view(db) == {"center": [10.0, 20.0], "zoom": 3, "max_zoom": 12}
374+
375+
376+
def test_json_db_get_initial_view_empty():
377+
assert json_db_get_initial_view(in_memory_json_db({})) == {}
378+
379+
380+
@mock.patch("platzky.db.google_json_db.Client")
381+
def test_google_json_db_get_initial_view(mock_cli):
382+
blob = mock_cli.return_value.bucket.return_value.blob.return_value
383+
blob.download_as_text.return_value = json.dumps(
384+
{"map": {"initial_view": {"center": [53.37, 22.89], "max_zoom": 17}}}
385+
)
386+
db = GoogleJsonDb("bucket", "blob")
387+
assert google_json_db_get_initial_view(db) == {"center": [53.37, 22.89], "max_zoom": 17}
388+
389+
390+
@mock.patch("platzky.db.google_json_db.Client")
391+
def test_google_json_db_get_initial_view_empty(mock_cli):
392+
mock_cli.return_value.bucket.return_value.blob.return_value.download_as_text.return_value = (
393+
json.dumps({"map": {}})
394+
)
395+
db = GoogleJsonDb("bucket", "blob")
396+
assert google_json_db_get_initial_view(db) == {}
397+
398+
399+
@mock.patch("platzky.db.mongodb_db.MongoClient")
400+
def test_mongodb_db_get_initial_view(mock_client):
401+
mock_db = mock.Mock()
402+
mock_client.return_value.__getitem__.return_value = mock_db
403+
mock_db.config.find_one.return_value = {
404+
"_id": "map_config",
405+
"initial_view": {"center": [53.37, 22.89], "zoom": 8},
406+
}
407+
408+
db = MongoDB("mongodb://localhost:27017", "test_db")
409+
assert mongodb_db_get_initial_view(db) == {"center": [53.37, 22.89], "zoom": 8}
410+
411+
412+
@mock.patch("platzky.db.mongodb_db.MongoClient")
413+
def test_mongodb_db_get_initial_view_empty(mock_client):
414+
mock_db = mock.Mock()
415+
mock_client.return_value.__getitem__.return_value = mock_db
416+
mock_db.config.find_one.return_value = {"_id": "map_config"}
417+
418+
db = MongoDB("mongodb://localhost:27017", "test_db")
419+
assert mongodb_db_get_initial_view(db) == {}
420+
421+
422+
@mock.patch("platzky.db.mongodb_db.MongoClient")
423+
def test_mongodb_db_get_initial_view_no_config(mock_client):
424+
mock_db = mock.Mock()
425+
mock_client.return_value.__getitem__.return_value = mock_db
426+
mock_db.config.find_one.return_value = None
427+
428+
db = MongoDB("mongodb://localhost:27017", "test_db")
429+
assert mongodb_db_get_initial_view(db) == {}
430+
431+
343432
# Test get_visible_data and get_meta_data for google_json_db
344433
@mock.patch("platzky.db.google_json_db.Client")
345434
def test_google_json_db_get_visible_data(mock_cli):

‎tests/unit_tests/test_goodmap.py‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from goodmap import goodmap
1919
from goodmap.config import GoodmapConfig
2020
from goodmap.feature_flags import EnableAdminPanel
21+
from goodmap.initial_view import DEFAULT_CENTER, DEFAULT_MAX_ZOOM, DEFAULT_ZOOM
2122
from goodmap.plugin import (
2223
CAPABILITY_BASES,
2324
MapOverlayPluginBase,
@@ -45,9 +46,11 @@ def test_create_app_from_config():
4546
patch("goodmap.goodmap.get_location_obligatory_fields", return_value=[]),
4647
patch("goodmap.goodmap.get_category_data") as mock_get_category_data,
4748
patch("goodmap.goodmap.get_marker_styles") as mock_get_marker_styles,
49+
patch("goodmap.goodmap.get_initial_view") as mock_get_initial_view,
4850
):
4951
mock_get_category_data.return_value.return_value = {"categories": {}}
5052
mock_get_marker_styles.return_value.return_value = {}
53+
mock_get_initial_view.return_value.return_value = {}
5154
goodmap.create_app_from_config(config)
5255
mock_platzky_app_creation.assert_called_once_with(
5356
config,
@@ -166,6 +169,69 @@ def test_map_route_marker_styles():
166169
assert 'window.MARKER_STYLES={"colors":{},"icons":{}};' in response.data.decode("utf-8")
167170

168171

172+
def test_map_route_initial_view():
173+
"""The map opens where the data source says, via window.INITIAL_VIEW.
174+
175+
The view is resolved server-side into a complete {center, zoom, max_zoom}, so the
176+
frontend never has to merge a partial declaration against defaults of its own.
177+
"""
178+
app = goodmap.create_app_from_config(
179+
GoodmapConfig(
180+
APP_NAME="test_app",
181+
SECRET_KEY="test_secret",
182+
USE_WWW=False,
183+
BLOG_PREFIX="/blog",
184+
DB=JsonDbConfig(
185+
DATA={
186+
"site_content": {"pages": []},
187+
"initial_view": {"center": [53.37, 22.89], "zoom": 8, "max_zoom": 17},
188+
},
189+
TYPE="json",
190+
),
191+
)
192+
)
193+
app.config["WTF_CSRF_ENABLED"] = False # NOSONAR
194+
195+
response_text = app.test_client().get("/map").data.decode("utf-8")
196+
match = re.search(r"window\.INITIAL_VIEW\s*=\s*(.*?);", response_text)
197+
assert match is not None
198+
assert json.loads(match.group(1)) == {
199+
"center": [53.37, 22.89],
200+
"zoom": 8,
201+
"max_zoom": 17,
202+
}
203+
204+
205+
def test_map_route_initial_view_defaults_when_the_data_source_declares_none():
206+
"""Declaring a view is optional, and the page still gets a complete one - which is what
207+
lets the frontend treat a missing window.INITIAL_VIEW as an error case rather than a
208+
routine one."""
209+
app = goodmap.create_app_from_config(_minimal_config())
210+
app.config["WTF_CSRF_ENABLED"] = False # NOSONAR
211+
212+
response_text = app.test_client().get("/map").data.decode("utf-8")
213+
match = re.search(r"window\.INITIAL_VIEW\s*=\s*(.*?);", response_text)
214+
assert match is not None
215+
assert json.loads(match.group(1)) == {
216+
"center": list(DEFAULT_CENTER),
217+
"zoom": DEFAULT_ZOOM,
218+
"max_zoom": DEFAULT_MAX_ZOOM,
219+
}
220+
221+
222+
def test_map_route_initial_view_stays_in_step_with_startup():
223+
"""Resolved once at startup, like marker_styles: a view cannot change without a
224+
restart, and a per-request read would only add a chance of the two disagreeing."""
225+
app = goodmap.create_app_from_config(_minimal_config())
226+
app.config["WTF_CSRF_ENABLED"] = False # NOSONAR
227+
228+
with mock.patch.object(app.db, "get_initial_view") as fresh_read:
229+
response_text = app.test_client().get("/map").data.decode("utf-8")
230+
231+
fresh_read.assert_not_called()
232+
assert "INITIAL_VIEW" in response_text
233+
234+
169235
def test_map_route_marker_styles_stay_in_step_with_the_api():
170236
"""window.MARKER_STYLES comes from the startup-time config, not a fresh read per
171237
request. The field /api/locations reads marker.icon from is fixed at startup, so a

0 commit comments

Comments
 (0)