From 1a647bec56cc3a269a9aa3ffbf56405719498281 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 23 Jul 2024 07:49:17 +0100 Subject: [PATCH 1/5] Namespace all flask-admin configuration --- doc/advanced.rst | 12 +++++++--- examples/geo_alchemy/config.py | 8 +++---- flask_admin/contrib/sqla/view.py | 4 ++-- flask_admin/model/base.py | 2 +- flask_admin/static/admin/js/form.js | 16 +++++++------- .../templates/bootstrap2/admin/lib.html | 22 +++++++++---------- .../templates/bootstrap3/admin/lib.html | 22 +++++++++---------- .../templates/bootstrap4/admin/lib.html | 22 +++++++++---------- 8 files changed, 57 insertions(+), 51 deletions(-) diff --git a/doc/advanced.rst b/doc/advanced.rst index 93be11b83..9fd41e357 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -280,14 +280,20 @@ To have map data display correctly, you'll have to sign up for an account at htt and include some credentials in your application's config:: app = Flask(__name__) - app.config['MAPBOX_MAP_ID'] = "example.abc123" - app.config['MAPBOX_ACCESS_TOKEN'] = "pk.def456" - + app.config['FLASK_ADMIN_MAPBOX_MAP_ID'] = "example.abc123" + app.config['FLASK_ADMIN_MAPBOX_ACCESS_TOKEN'] = "pk.def456" + app.config['FLASK_ADMIN_DEFAULT_CENTER_LAT'] = -33.918861 # Replace with your own value + app.config['FLASK_ADMIN_DEFAULT_CENTER_LONG'] = 18.423300 # Replace with your own value Leaflet supports loading map tiles from any arbitrary map tile provider, but at the moment, Flask-Admin only supports Mapbox. If you want to use other providers, make a pull request! +If you want to include a search box on map widgets for looking up locations, you need the following additional configuration:: + + app.config['FLASK_ADMIN_MAPBOX_SEARCH'] = True + app.config['FLASK_ADMIN_GOOGLE_MAPS_API_KEY'] = 'secret' + Limitations *********** diff --git a/examples/geo_alchemy/config.py b/examples/geo_alchemy/config.py index 54126793f..1e21cee3d 100644 --- a/examples/geo_alchemy/config.py +++ b/examples/geo_alchemy/config.py @@ -6,9 +6,9 @@ SQLALCHEMY_ECHO = True # credentials for loading map tiles from mapbox -MAPBOX_MAP_ID = 'light-v10' # example map id -MAPBOX_ACCESS_TOKEN = '...' +FLASK_ADMIN_MAPBOX_MAP_ID = 'light-v10' # example map id +FLASK_ADMIN_MAPBOX_ACCESS_TOKEN = '...' # when the creating new shapes, use this default map center -DEFAULT_CENTER_LAT = -33.918861 -DEFAULT_CENTER_LONG = 18.423300 +FLASK_ADMIN_DEFAULT_CENTER_LAT = -33.918861 +FLASK_ADMIN_DEFAULT_CENTER_LONG = 18.423300 diff --git a/flask_admin/contrib/sqla/view.py b/flask_admin/contrib/sqla/view.py index 9b4622d5b..531985832 100755 --- a/flask_admin/contrib/sqla/view.py +++ b/flask_admin/contrib/sqla/view.py @@ -1126,8 +1126,8 @@ def get_one(self, id): def handle_view_exception(self, exc): if isinstance(exc, IntegrityError): if current_app.config.get( - 'ADMIN_RAISE_ON_INTEGRITY_ERROR', - current_app.config.get('ADMIN_RAISE_ON_VIEW_EXCEPTION') + 'FLASK_ADMIN_RAISE_ON_INTEGRITY_ERROR', + current_app.config.get('FLASK_ADMIN_RAISE_ON_VIEW_EXCEPTION') ): raise else: diff --git a/flask_admin/model/base.py b/flask_admin/model/base.py index 7a053bf93..920cc8531 100755 --- a/flask_admin/model/base.py +++ b/flask_admin/model/base.py @@ -1550,7 +1550,7 @@ def handle_view_exception(self, exc): flash(as_unicode(exc), 'error') return True - if current_app.config.get('ADMIN_RAISE_ON_VIEW_EXCEPTION'): + if current_app.config.get('FLASK_ADMIN_RAISE_ON_VIEW_EXCEPTION'): raise if self._debug: diff --git a/flask_admin/static/admin/js/form.js b/flask_admin/static/admin/js/form.js index 0f1176d6a..d298d8c4f 100644 --- a/flask_admin/static/admin/js/form.js +++ b/flask_admin/static/admin/js/form.js @@ -74,12 +74,12 @@ * Process Leaflet (map) widget */ function processLeafletWidget($el, name) { - if (!window.MAPBOX_MAP_ID) { - console.error("You must set MAPBOX_MAP_ID in your Flask settings to use the map widget"); + if (!window.FLASK_ADMIN_MAPBOX_MAP_ID) { + console.error("You must set FLASK_ADMIN_MAPBOX_MAP_ID in your Flask settings to use the map widget"); return false; } - if (!window.DEFAULT_CENTER_LAT || !window.DEFAULT_CENTER_LONG) { - console.error("You must set DEFAULT_CENTER_LAT and DEFAULT_CENTER_LONG in your Flask settings to use the map widget"); + if (!window.FLASK_ADMIN_DEFAULT_CENTER_LAT || !window.FLASK_ADMIN_DEFAULT_CENTER_LONG) { + console.error("You must set FLASK_ADMIN_DEFAULT_CENTER_LAT and FLASK_ADMIN_DEFAULT_CENTER_LONG in your Flask settings to use the map widget"); return false; } @@ -154,11 +154,11 @@ } } else { // use the default map center - map.setView([window.DEFAULT_CENTER_LAT, window.DEFAULT_CENTER_LONG], 12); + map.setView([window.FLASK_ADMIN_DEFAULT_CENTER_LAT, window.FLASK_ADMIN_DEFAULT_CENTER_LONG], 12); } // set up tiles - var mapboxHostnameAndPath = $el.data('tile-layer-url') || 'api.mapbox.com/styles/v1/mapbox/'+window.MAPBOX_MAP_ID+'/tiles/{z}/{x}/{y}?access_token={accessToken}'; + var mapboxHostnameAndPath = $el.data('tile-layer-url') || 'api.mapbox.com/styles/v1/mapbox/'+window.FLASK_ADMIN_MAPBOX_MAP_ID+'/tiles/{z}/{x}/{y}?access_token={accessToken}'; var attribution = $el.data('tile-layer-attribution') || 'Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © Mapbox'; L.tileLayer('//' + mapboxHostnameAndPath, { // Attributes from https://docs.mapbox.com/help/troubleshooting/migrate-legacy-static-tiles-api/ @@ -166,7 +166,7 @@ maxZoom: 18, tileSize: 512, zoomOffset: -1, - accessToken: window.MAPBOX_ACCESS_TOKEN + accessToken: window.FLASK_ADMIN_MAPBOX_ACCESS_TOKEN }).addTo(map); // everything below here is to set up editing, so if we're not editable, @@ -201,7 +201,7 @@ } var drawControl = new L.Control.Draw(drawOptions); map.addControl(drawControl); - if (window.MAPBOX_SEARCH) { + if (window.FLASK_ADMIN_MAPBOX_SEARCH) { var circle = L.circleMarker([0, 0]); var $autocompleteEl = $(''); var $form = $($el.get(0).form); diff --git a/flask_admin/templates/bootstrap2/admin/lib.html b/flask_admin/templates/bootstrap2/admin/lib.html index f05d5f93e..37d50cbec 100644 --- a/flask_admin/templates/bootstrap2/admin/lib.html +++ b/flask_admin/templates/bootstrap2/admin/lib.html @@ -218,7 +218,7 @@

{{ text }}

{% macro form_css() %} - {% if config.MAPBOX_MAP_ID %} + {% if config.FLASK_ADMIN_MAPBOX_MAP_ID %} {% endif %} @@ -228,24 +228,24 @@

{{ text }}

{% endmacro %} {% macro form_js() %} - {% if config.MAPBOX_MAP_ID %} + {% if config.FLASK_ADMIN_MAPBOX_MAP_ID %} - {% if config.MAPBOX_SEARCH %} + {% if config.FLASK_ADMIN_MAPBOX_SEARCH %} - + {% endif %} {% endif %} diff --git a/flask_admin/templates/bootstrap3/admin/lib.html b/flask_admin/templates/bootstrap3/admin/lib.html index 6bfdb05a6..c69f589ed 100644 --- a/flask_admin/templates/bootstrap3/admin/lib.html +++ b/flask_admin/templates/bootstrap3/admin/lib.html @@ -209,7 +209,7 @@

{{ text }}

- {% if config.MAPBOX_MAP_ID %} + {% if config.FLASK_ADMIN_MAPBOX_MAP_ID %} {% endif %} @@ -219,24 +219,24 @@

{{ text }}

{% endmacro %} {% macro form_js() %} - {% if config.MAPBOX_MAP_ID %} + {% if config.FLASK_ADMIN_MAPBOX_MAP_ID %} - {% if config.MAPBOX_SEARCH %} + {% if config.FLASK_ADMIN_MAPBOX_SEARCH %} - + {% endif %} {% endif %} diff --git a/flask_admin/templates/bootstrap4/admin/lib.html b/flask_admin/templates/bootstrap4/admin/lib.html index a4d0c37c9..0b021784d 100644 --- a/flask_admin/templates/bootstrap4/admin/lib.html +++ b/flask_admin/templates/bootstrap4/admin/lib.html @@ -247,7 +247,7 @@

{{ text }}

- {% if config.MAPBOX_MAP_ID %} + {% if config.FLASK_ADMIN_MAPBOX_MAP_ID %} {% endif %} @@ -257,24 +257,24 @@

{{ text }}

{% endmacro %} {% macro form_js() %} - {% if config.MAPBOX_MAP_ID %} + {% if config.FLASK_ADMIN_MAPBOX_MAP_ID %} - {% if config.MAPBOX_SEARCH %} + {% if config.FLASK_ADMIN_MAPBOX_SEARCH %} - + {% endif %} {% endif %} From 48ba66d8c39b09a61c476444a80718baab15f75c Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 23 Jul 2024 07:59:09 +0100 Subject: [PATCH 2/5] Add mapbox config to geoalchemy example --- examples/geo_alchemy/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/geo_alchemy/config.py b/examples/geo_alchemy/config.py index 1e21cee3d..f0df2dcd7 100644 --- a/examples/geo_alchemy/config.py +++ b/examples/geo_alchemy/config.py @@ -12,3 +12,6 @@ # when the creating new shapes, use this default map center FLASK_ADMIN_DEFAULT_CENTER_LAT = -33.918861 FLASK_ADMIN_DEFAULT_CENTER_LONG = 18.423300 + +FLASK_ADMIN_MAPBOX_SEARCH = True +FLASK_ADMIN_GOOGLE_MAPS_API_KEY = '...' From 7bf6b728f1c00c64915a91bdbc52c09413be87e8 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 23 Jul 2024 08:42:28 +0100 Subject: [PATCH 3/5] Add documentation around missing env vars --- doc/advanced.rst | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/doc/advanced.rst b/doc/advanced.rst index 9fd41e357..fbcdf4d25 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -611,3 +611,34 @@ While the wrapped function should accept only one parameter - `ids`:: raise flash(gettext('Failed to approve users. %(error)s', error=str(ex)), 'error') + + +Raise exceptions instead of flash error messages +------------------------------------------------ + +**** + +By default, Flask-Admin will capture most exceptions related to reading/writing models +and display a flash message instead of raising an exception. If your Flask app is running +in debug mode (ie under local development), exceptions will not be suppressed. + +The flash message behaviour can be overridden with some Flask configuration.:: + + app = Flask(__name__) + app.config['FLASK_ADMIN_RAISE_ON_VIEW_EXCEPTION'] = True + app.config['FLASK_ADMIN_RAISE_ON_INTEGRITY_ERROR'] = True + + +FLASK_ADMIN_RAISE_ON_VIEW_EXCEPTION +*********************************** +Instead of turning exceptions on model create/update/delete actions into flash messages, +raise the exception as normal. You should expect the view to return a 500 to the user, +unless you add specific handling to prevent this. + +FLASK_ADMIN_RAISE_ON_INTEGRITY_ERROR +************************************ +This targets SQLAlchemy specifically. + +Unlike the previous setting, this will specifically only affect the behaviour of +IntegrityErrors. These usually come from violations on constraints in the database, +for example trying to insert a row with a primary key that already exists. From 40ad8050a36fdbf1246a7a997a61ee92532f3007 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 23 Jul 2024 09:36:58 +0100 Subject: [PATCH 4/5] Rename the conditional maps config variable Previously the Flask app config option `FLASK_ADMIN_MAPBOX_MAP_ID` was required to enable map widgets, even if MapBox wasn't being used. Let's clear up this confusion by having a clearer config name for toggling maps on/off: `FLASK_ADMIN_MAPS`. And then finally update the documentation around maps to talk about this new configuration, and also talk about how to override the default MapBox integration. Finally, we update the geoalchemy example app to have some pages use the default MapBox integration and some use OSM instead. --- doc/advanced.rst | 33 +++++++++++++------ examples/geo_alchemy/app.py | 19 +++++++---- examples/geo_alchemy/config.py | 3 +- flask_admin/static/admin/js/form.js | 6 ++-- .../templates/bootstrap2/admin/lib.html | 9 ++--- .../templates/bootstrap3/admin/lib.html | 9 ++--- .../templates/bootstrap4/admin/lib.html | 9 ++--- 7 files changed, 55 insertions(+), 33 deletions(-) diff --git a/doc/advanced.rst b/doc/advanced.rst index fbcdf4d25..9409c808d 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -273,27 +273,40 @@ Some of the Geometry field types that are available include: Have a look at https://github.com/flask-admin/flask-admin/tree/master/examples/geo_alchemy to get started. -Loading Tiles From Mapbox -************************* +Display map widgets +******************* + +Flask-Admin uses `Leaflet `_ to display map widgets for +geographical data. By default, this uses `MapBox `_. -To have map data display correctly, you'll have to sign up for an account at https://www.mapbox.com/ -and include some credentials in your application's config:: +To have MapBox data display correctly, you'll have to sign up for an account and include +some credentials in your application's config:: app = Flask(__name__) + app.config['FLASK_ADMIN_MAPS'] = True + + # Required: configure the default centre position for blank maps + app.config['FLASK_ADMIN_DEFAULT_CENTER_LAT'] = -33.918861 + app.config['FLASK_ADMIN_DEFAULT_CENTER_LONG'] = 18.423300 + + # Required if using the default Mapbox integration app.config['FLASK_ADMIN_MAPBOX_MAP_ID'] = "example.abc123" app.config['FLASK_ADMIN_MAPBOX_ACCESS_TOKEN'] = "pk.def456" - app.config['FLASK_ADMIN_DEFAULT_CENTER_LAT'] = -33.918861 # Replace with your own value - app.config['FLASK_ADMIN_DEFAULT_CENTER_LONG'] = 18.423300 # Replace with your own value -Leaflet supports loading map tiles from any arbitrary map tile provider, but -at the moment, Flask-Admin only supports Mapbox. If you want to use other -providers, make a pull request! +If you want to use a map provider other than MapBox (eg OpenStreetMaps), you can override +the tile layer URLs and tile attribution attributes:: + + class CityView(ModelView): + tile_layer_url = '{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' + tile_layer_attribution = '© OpenStreetMap contributors' If you want to include a search box on map widgets for looking up locations, you need the following additional configuration:: - app.config['FLASK_ADMIN_MAPBOX_SEARCH'] = True + app.config['FLASK_ADMIN_MAPS_SEARCH'] = True app.config['FLASK_ADMIN_GOOGLE_MAPS_API_KEY'] = 'secret' +Flask-Admin currently only supports Google Maps for map search. + Limitations *********** diff --git a/examples/geo_alchemy/app.py b/examples/geo_alchemy/app.py index 028210afc..9257e537d 100644 --- a/examples/geo_alchemy/app.py +++ b/examples/geo_alchemy/app.py @@ -61,16 +61,21 @@ def index(): admin = admin.Admin(app, name='Example: GeoAlchemy', theme=Bootstrap4Theme()) -class ModalModelView(ModelView): +class LeafletModelView(ModelView): edit_modal = True + +class OSMModelView(ModelView): + tile_layer_url = '{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' + tile_layer_attribution = '© OpenStreetMap contributors' + # Add views -admin.add_view(ModalModelView(Point, db.session, category='Points')) -admin.add_view(ModalModelView(MultiPoint, db.session, category='Points')) -admin.add_view(ModalModelView(Polygon, db.session, category='Polygons')) -admin.add_view(ModalModelView(MultiPolygon, db.session, category='Polygons')) -admin.add_view(ModalModelView(LineString, db.session, category='Lines')) -admin.add_view(ModalModelView(MultiLineString, db.session, category='Lines')) +admin.add_view(LeafletModelView(Point, db.session, category='Points')) +admin.add_view(OSMModelView(MultiPoint, db.session, category='Points')) +admin.add_view(LeafletModelView(Polygon, db.session, category='Polygons')) +admin.add_view(OSMModelView(MultiPolygon, db.session, category='Polygons')) +admin.add_view(LeafletModelView(LineString, db.session, category='Lines')) +admin.add_view(OSMModelView(MultiLineString, db.session, category='Lines')) if __name__ == '__main__': diff --git a/examples/geo_alchemy/config.py b/examples/geo_alchemy/config.py index f0df2dcd7..29d529f6a 100644 --- a/examples/geo_alchemy/config.py +++ b/examples/geo_alchemy/config.py @@ -6,6 +6,8 @@ SQLALCHEMY_ECHO = True # credentials for loading map tiles from mapbox +FLASK_ADMIN_MAPS = True +FLASK_ADMIN_MAPS_SEARCH = False FLASK_ADMIN_MAPBOX_MAP_ID = 'light-v10' # example map id FLASK_ADMIN_MAPBOX_ACCESS_TOKEN = '...' @@ -13,5 +15,4 @@ FLASK_ADMIN_DEFAULT_CENTER_LAT = -33.918861 FLASK_ADMIN_DEFAULT_CENTER_LONG = 18.423300 -FLASK_ADMIN_MAPBOX_SEARCH = True FLASK_ADMIN_GOOGLE_MAPS_API_KEY = '...' diff --git a/flask_admin/static/admin/js/form.js b/flask_admin/static/admin/js/form.js index d298d8c4f..d16f8accd 100644 --- a/flask_admin/static/admin/js/form.js +++ b/flask_admin/static/admin/js/form.js @@ -74,8 +74,8 @@ * Process Leaflet (map) widget */ function processLeafletWidget($el, name) { - if (!window.FLASK_ADMIN_MAPBOX_MAP_ID) { - console.error("You must set FLASK_ADMIN_MAPBOX_MAP_ID in your Flask settings to use the map widget"); + if (!window.FLASK_ADMIN_MAPS) { + console.error("You must set FLASK_ADMIN_MAPS in your Flask settings to use the map widget"); return false; } if (!window.FLASK_ADMIN_DEFAULT_CENTER_LAT || !window.FLASK_ADMIN_DEFAULT_CENTER_LONG) { @@ -201,7 +201,7 @@ } var drawControl = new L.Control.Draw(drawOptions); map.addControl(drawControl); - if (window.FLASK_ADMIN_MAPBOX_SEARCH) { + if (window.FLASK_ADMIN_MAPS_SEARCH) { var circle = L.circleMarker([0, 0]); var $autocompleteEl = $(''); var $form = $($el.get(0).form); diff --git a/flask_admin/templates/bootstrap2/admin/lib.html b/flask_admin/templates/bootstrap2/admin/lib.html index 37d50cbec..b00bb9ad8 100644 --- a/flask_admin/templates/bootstrap2/admin/lib.html +++ b/flask_admin/templates/bootstrap2/admin/lib.html @@ -218,7 +218,7 @@

{{ text }}

{% macro form_css() %} - {% if config.FLASK_ADMIN_MAPBOX_MAP_ID %} + {% if config.FLASK_ADMIN_MAPS %} {% endif %} @@ -228,8 +228,9 @@

{{ text }}

{% endmacro %} {% macro form_js() %} - {% if config.FLASK_ADMIN_MAPBOX_MAP_ID %} + {% if config.FLASK_ADMIN_MAPS %} - {% if config.FLASK_ADMIN_MAPBOX_SEARCH %} + {% if config.FLASK_ADMIN_MAPS_SEARCH %} {% endif %} diff --git a/flask_admin/templates/bootstrap3/admin/lib.html b/flask_admin/templates/bootstrap3/admin/lib.html index c69f589ed..d3a441118 100644 --- a/flask_admin/templates/bootstrap3/admin/lib.html +++ b/flask_admin/templates/bootstrap3/admin/lib.html @@ -209,7 +209,7 @@

{{ text }}

- {% if config.FLASK_ADMIN_MAPBOX_MAP_ID %} + {% if config.FLASK_ADMIN_MAPS %} {% endif %} @@ -219,8 +219,9 @@

{{ text }}

{% endmacro %} {% macro form_js() %} - {% if config.FLASK_ADMIN_MAPBOX_MAP_ID %} + {% if config.FLASK_ADMIN_MAPS %} - {% if config.FLASK_ADMIN_MAPBOX_SEARCH %} + {% if config.FLASK_ADMIN_MAPS_SEARCH %} {% endif %} diff --git a/flask_admin/templates/bootstrap4/admin/lib.html b/flask_admin/templates/bootstrap4/admin/lib.html index 0b021784d..7574ff9a6 100644 --- a/flask_admin/templates/bootstrap4/admin/lib.html +++ b/flask_admin/templates/bootstrap4/admin/lib.html @@ -247,7 +247,7 @@

{{ text }}

- {% if config.FLASK_ADMIN_MAPBOX_MAP_ID %} + {% if config.FLASK_ADMIN_MAPS %} {% endif %} @@ -257,8 +257,9 @@

{{ text }}

{% endmacro %} {% macro form_js() %} - {% if config.FLASK_ADMIN_MAPBOX_MAP_ID %} + {% if config.FLASK_ADMIN_MAPS %} - {% if config.FLASK_ADMIN_MAPBOX_SEARCH %} + {% if config.FLASK_ADMIN_MAPS_SEARCH %} {% endif %} From 3c0d67a38e813825de2c085a3a123352bd61ee47 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 23 Jul 2024 20:46:04 +0100 Subject: [PATCH 5/5] Update geo_alchemy example docs --- examples/geo_alchemy/README.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/geo_alchemy/README.rst b/examples/geo_alchemy/README.rst index 40a8a58c1..1b9361b82 100644 --- a/examples/geo_alchemy/README.rst +++ b/examples/geo_alchemy/README.rst @@ -34,6 +34,10 @@ To run this example: python examples/geo_alchemy/app.py -6. You will notice that the maps are not rendered. To see them, you will have +6. You will notice that the maps are not rendered. By default, Flask-Admin expects +an integration with `Mapbox `_. To see them, you will have to register for a free account at `Mapbox `_ and set -the *MAPBOX_MAP_ID* and *MAPBOX_ACCESS_TOKEN* config variables accordingly. +the *FLASK_ADMIN_MAPBOX_MAP_ID* and *FLASK_ADMIN_MAPBOX_ACCESS_TOKEN* config +variables accordingly. + +However, some of the maps are overridden to use Open Street Maps