Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/en_US/preferences.rst
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,12 @@ provider used as a base layer when viewing geometry data.
When a custom tile provider is configured, it is selected as the default
base layer of the Geometry Viewer.

An administrator may set a system-wide default for these fields via the
``DEFAULT_GEOMETRY_VIEWER_PROVIDER`` setting in :ref:`config_py`. It applies
to any user who has not saved their own values for the fields above; a
user's own preferences, once saved, always take precedence over the
system-wide default.

.. image:: images/preferences_graph_visualiser.png
:alt: Preferences sqleditor graph visualiser section
:align: center
Expand Down
20 changes: 20 additions & 0 deletions web/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,26 @@
##########################################################################
DATA_RESULT_ROWS_PER_PAGE = 1000

##########################################################################
# System-wide default for the Geometry Viewer's custom tile provider
# (Query Tool > Data Output > Geometry Viewer). Applies to any user who
# has not saved their own "Custom tile provider ..." preference; a
# per-user preference, once set, always takes precedence over this
# default. Leave "url" empty to keep using the built-in tile layers by
# default (the pre-existing behaviour).
#
# "crs" must be one of "EPSG:3857" (Web Mercator), "EPSG:4326", or
# "EPSG:3395" - the only coordinate reference systems the Geometry
# Viewer's tile layer selector supports.
##########################################################################
DEFAULT_GEOMETRY_VIEWER_PROVIDER = {
"url": "",
"name": "Custom",
"crs": "EPSG:3857",
"attribution": "",
"max_zoom": 18
}

##########################################################################
# Allow users to display Gravatar image for their username in Server mode
##########################################################################
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################

from unittest.mock import patch

from pgadmin.utils.route import BaseTestGenerator
from pgadmin.tools.sqleditor.utils import query_tool_preferences as qtp


class GeometryViewerProviderDefaultsTestCase(BaseTestGenerator):
"""
Verify resolve_geometry_viewer_provider_defaults() safely resolves
config.DEFAULT_GEOMETRY_VIEWER_PROVIDER, since config_local.py /
config_distro.py / PGADMIN_CONFIG_* replace that variable wholesale
rather than merging individual keys - a partial, wrong-typed, or
out-of-range administrator override must fall back to the original
hardcoded defaults instead of crashing preference registration.
"""

_FULL_VALID = {
'url': 'https://internal.example.com/tiles/{z}/{x}/{y}.png',
'name': 'Internal',
'crs': 'EPSG:4326',
'attribution': 'Internal Corp',
'max_zoom': 12,
}

# What every field falls back to when the admin-supplied value for it
# is missing, the wrong type, or out of range.
_ALL_FALLBACK = {
'url': '', 'name': 'Custom', 'crs': 'EPSG:3857',
'attribution': '', 'max_zoom': 18,
}

_partial_override_expected = dict(_ALL_FALLBACK)
_partial_override_expected['url'] = \
'https://x.example.com/{z}/{x}/{y}.png'

scenarios = [
('Fully valid override is used as-is',
dict(provider=_FULL_VALID, expected=_FULL_VALID)),

('Partial override falls back for missing keys',
dict(provider={'url': 'https://x.example.com/{z}/{x}/{y}.png'},
expected=_partial_override_expected)),

('Non-dict override falls back entirely',
dict(provider='oops, not a dict', expected=_ALL_FALLBACK)),

('Unsupported CRS falls back to EPSG:3857',
dict(provider={'crs': 'EPSG:9999'}, expected=_ALL_FALLBACK)),

('Out-of-range max_zoom falls back to 18',
dict(provider={'max_zoom': 999}, expected=_ALL_FALLBACK)),

('Non-integer max_zoom falls back to 18',
dict(provider={'max_zoom': '18'}, expected=_ALL_FALLBACK)),

('Boolean max_zoom falls back to 18',
dict(provider={'max_zoom': True}, expected=_ALL_FALLBACK)),

('Non-string name/url/attribution fall back individually',
dict(provider={'name': 123, 'url': 456, 'attribution': 789},
expected=_ALL_FALLBACK)),

('Empty dict falls back entirely',
dict(provider={}, expected=_ALL_FALLBACK)),
]

def runTest(self):
with patch.object(qtp.config, 'DEFAULT_GEOMETRY_VIEWER_PROVIDER',
self.provider):
result = qtp.resolve_geometry_viewer_provider_defaults()

self.assertEqual(result, self.expected)
61 changes: 55 additions & 6 deletions web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
##########################################################################

"""Register preferences for query tool"""
import config
from flask_babel import gettext
from pgadmin.utils.constants import PREF_LABEL_DISPLAY, \
PREF_LABEL_KEYBOARD_SHORTCUTS, PREF_LABEL_EXPLAIN, PREF_LABEL_OPTIONS, \
Expand All @@ -16,8 +17,49 @@
from pgadmin.utils import SHORTCUT_FIELDS as shortcut_fields
from config import DATA_RESULT_ROWS_PER_PAGE

GEOMETRY_VIEWER_CRS_CHOICES = ('EPSG:3857', 'EPSG:4326', 'EPSG:3395')


def resolve_geometry_viewer_provider_defaults():
"""Resolve config.DEFAULT_GEOMETRY_VIEWER_PROVIDER into safe defaults
for the geometry_viewer preferences.

config_local.py/config_distro.py/PGADMIN_CONFIG_* replace this single
config.py variable wholesale rather than merging individual keys, so
an administrator overriding only some fields, or setting the wrong
type entirely (e.g. a string instead of a dict), must not crash
preference registration for the rest, and an out-of-range/unsupported
value must not silently reach the frontend unvalidated.
"""
provider = config.DEFAULT_GEOMETRY_VIEWER_PROVIDER
if not isinstance(provider, dict):
provider = {}

def _str_default(key, fallback):
value = provider.get(key, fallback)
return value if isinstance(value, str) else fallback

crs = provider.get('crs', 'EPSG:3857')
if crs not in GEOMETRY_VIEWER_CRS_CHOICES:
crs = 'EPSG:3857'

max_zoom = provider.get('max_zoom', 18)
if not isinstance(max_zoom, int) or isinstance(max_zoom, bool) or \
not 0 <= max_zoom <= 25:
max_zoom = 18

return {
'url': _str_default('url', ''),
'name': _str_default('name', 'Custom'),
'crs': crs,
'attribution': _str_default('attribution', ''),
'max_zoom': max_zoom,
}


def register_query_tool_preferences(self):
geometry_viewer_defaults = resolve_geometry_viewer_provider_defaults()

self.explain_verbose = self.preference.register(
'Explain', 'explain_verbose',
gettext("Verbose output?"), 'boolean', False,
Expand Down Expand Up @@ -841,7 +883,8 @@ def register_query_tool_preferences(self):

self.custom_tile_url = self.preference.register(
'geometry_viewer', 'custom_tile_url',
gettext("Custom tile provider URL"), 'text', '',
gettext("Custom tile provider URL"), 'text',
geometry_viewer_defaults['url'],
category_label=PREF_LABEL_GEOMETRY_VIEWER,
control_props={
'placeholder': 'https://{s}.tile.example.com/{z}/{x}/{y}.png',
Expand All @@ -853,13 +896,16 @@ def register_query_tool_preferences(self):
'.png. The template must contain the {x}, {y} and '
'{z} placeholders, and may contain {s} for '
'subdomains (a, b, c). Leave empty to disable the '
'custom tile provider.'),
'custom tile provider. Defaults to the '
'administrator-configured '
'DEFAULT_GEOMETRY_VIEWER_PROVIDER, if any.'),
allow_blanks=True
)

self.custom_tile_name = self.preference.register(
'geometry_viewer', 'custom_tile_name',
gettext("Custom tile provider name"), 'text', 'Custom',
gettext("Custom tile provider name"), 'text',
geometry_viewer_defaults['name'],
category_label=PREF_LABEL_GEOMETRY_VIEWER,
help_str=gettext('Display name of the custom tile provider in the '
'layer selector of the Geometry Viewer.'),
Expand All @@ -868,7 +914,8 @@ def register_query_tool_preferences(self):

self.custom_tile_crs = self.preference.register(
'geometry_viewer', 'custom_tile_crs',
gettext("Custom tile provider CRS"), 'options', 'EPSG:3857',
gettext("Custom tile provider CRS"), 'options',
geometry_viewer_defaults['crs'],
category_label=PREF_LABEL_GEOMETRY_VIEWER,
options=[{'label': gettext('EPSG:3857 (Web Mercator)'),
'value': 'EPSG:3857'},
Expand All @@ -886,7 +933,8 @@ def register_query_tool_preferences(self):

self.custom_tile_attribution = self.preference.register(
'geometry_viewer', 'custom_tile_attribution',
gettext("Custom tile provider attribution"), 'text', '',
gettext("Custom tile provider attribution"), 'text',
geometry_viewer_defaults['attribution'],
category_label=PREF_LABEL_GEOMETRY_VIEWER,
help_str=gettext('Attribution text shown on the map for the custom '
'tile provider. May contain HTML links.'),
Expand All @@ -895,7 +943,8 @@ def register_query_tool_preferences(self):

self.custom_tile_max_zoom = self.preference.register(
'geometry_viewer', 'custom_tile_max_zoom',
gettext("Custom tile provider max zoom"), 'integer', 18,
gettext("Custom tile provider max zoom"), 'integer',
geometry_viewer_defaults['max_zoom'],
min_val=0, max_val=25,
category_label=PREF_LABEL_GEOMETRY_VIEWER,
help_str=gettext('Maximum zoom level of the custom tile provider.')
Expand Down
Loading