c2cgeoportal-geoportal 2.6.0__py2.py3-none-any.whl → 2.8.1.180__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- c2cgeoportal_geoportal/__init__.py +245 -95
- c2cgeoportal_geoportal/lib/__init__.py +67 -43
- c2cgeoportal_geoportal/lib/authentication.py +50 -26
- c2cgeoportal_geoportal/lib/bashcolor.py +17 -13
- c2cgeoportal_geoportal/lib/cacheversion.py +16 -8
- c2cgeoportal_geoportal/lib/caching.py +65 -193
- c2cgeoportal_geoportal/lib/check_collector.py +17 -10
- c2cgeoportal_geoportal/lib/checker.py +67 -65
- c2cgeoportal_geoportal/lib/common_headers.py +167 -0
- c2cgeoportal_geoportal/lib/dbreflection.py +61 -46
- c2cgeoportal_geoportal/lib/filter_capabilities.py +126 -88
- c2cgeoportal_geoportal/lib/fulltextsearch.py +6 -5
- c2cgeoportal_geoportal/lib/functionality.py +20 -17
- c2cgeoportal_geoportal/lib/headers.py +14 -5
- c2cgeoportal_geoportal/lib/i18n.py +4 -4
- c2cgeoportal_geoportal/lib/layers.py +30 -11
- c2cgeoportal_geoportal/lib/lingua_extractor.py +363 -240
- c2cgeoportal_geoportal/lib/loader.py +11 -16
- c2cgeoportal_geoportal/lib/metrics.py +28 -17
- c2cgeoportal_geoportal/lib/oauth2.py +392 -206
- c2cgeoportal_geoportal/lib/wmstparsing.py +105 -84
- c2cgeoportal_geoportal/lib/xsd.py +26 -16
- c2cgeoportal_geoportal/resources.py +15 -9
- c2cgeoportal_geoportal/scaffolds/advance_create/ci/config.yaml +26 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/cookiecutter.json +18 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/.dockerignore +6 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/.eslintrc.yaml +19 -0
- c2cgeoportal_geoportal/scaffolds/{create/geoportal/+dot+prospector.yaml → advance_create/{{cookiecutter.project}}/geoportal/.prospector.yaml} +8 -2
- c2cgeoportal_geoportal/scaffolds/{create/geoportal/Dockerfile_tmpl → advance_create/{{cookiecutter.project}}/geoportal/Dockerfile} +22 -15
- c2cgeoportal_geoportal/scaffolds/{create/geoportal/alembic.yaml_tmpl → advance_create/{{cookiecutter.project}}/geoportal/alembic.yaml} +1 -1
- c2cgeoportal_geoportal/scaffolds/{create/geoportal/development.ini_tmpl → advance_create/{{cookiecutter.project}}/geoportal/development.ini} +34 -15
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/gunicorn.conf.py +100 -0
- c2cgeoportal_geoportal/scaffolds/{create → advance_create/{{cookiecutter.project}}}/geoportal/lingua-client.cfg +1 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/production.ini +38 -0
- c2cgeoportal_geoportal/scaffolds/{create/geoportal/setup.py_tmpl → advance_create/{{cookiecutter.project}}/geoportal/setup.py} +6 -7
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/tsconfig.json +8 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.api.js +77 -0
- c2cgeoportal_geoportal/scaffolds/{create/geoportal/webpack.apps.js_tmpl → advance_create/{{cookiecutter.project}}/geoportal/webpack.apps.js} +29 -28
- c2cgeoportal_geoportal/scaffolds/{create → advance_create/{{cookiecutter.project}}}/geoportal/webpack.commons.js +4 -7
- c2cgeoportal_geoportal/scaffolds/{create → advance_create/{{cookiecutter.project}}}/geoportal/webpack.config.js +1 -1
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/__init__.py +42 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/authentication.py +10 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/dev.py +14 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/models.py +8 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/multi_organization.py +7 -0
- c2cgeoportal_geoportal/scaffolds/{create/geoportal/+package+_geoportal → advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal}/resources.py +4 -3
- c2cgeoportal_geoportal/scaffolds/{create/geoportal/+package+_geoportal/static-ngeo/api/index.js_tmpl → advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static-ngeo/api/index.js} +1 -2
- c2cgeoportal_geoportal/scaffolds/{create/geoportal/+package+_geoportal/static-ngeo/js/+package+module.js_tmpl → advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static-ngeo/js/{{cookiecutter.package}}module.js} +4 -4
- c2cgeoportal_geoportal/scaffolds/{create/geoportal/+package+_geoportal/subscribers.py_tmpl → advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/subscribers.py} +1 -3
- c2cgeoportal_geoportal/scaffolds/advance_update/cookiecutter.json +18 -0
- c2cgeoportal_geoportal/scaffolds/{update/geoportal/CONST_Makefile_tmpl → advance_update/{{cookiecutter.project}}/geoportal/CONST_Makefile} +3 -27
- c2cgeoportal_geoportal/scaffolds/create/cookiecutter.json +18 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.dockerignore +14 -0
- c2cgeoportal_geoportal/scaffolds/create/{+dot+editorconfig → {{cookiecutter.project}}/.editorconfig} +2 -5
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/main.yaml +57 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/rebuild.yaml +46 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/update_l10n.yaml +66 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.gitignore +16 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.prettierignore +1 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.prettierrc.yaml +2 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Dockerfile +76 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Makefile +70 -0
- c2cgeoportal_geoportal/scaffolds/create/{README.rst_tmpl → {{cookiecutter.project}}/README.rst} +4 -4
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/build +186 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/config.yaml +22 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/docker-compose-check +25 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/requirements.txt +1 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-db.yaml +26 -0
- c2cgeoportal_geoportal/scaffolds/create/{docker-compose-lib.yaml → {{cookiecutter.project}}/docker-compose-lib.yaml} +165 -22
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-qgis.yaml +23 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose.override.sample.yaml +66 -0
- c2cgeoportal_geoportal/scaffolds/create/{docker-compose.yaml → {{cookiecutter.project}}/docker-compose.yaml} +20 -15
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.default +101 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.project +69 -0
- c2cgeoportal_geoportal/scaffolds/create/{geoportal/vars.yaml_tmpl → {{cookiecutter.project}}/geoportal/vars.yaml} +126 -36
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/css/mobile.css +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{mapserver → {{cookiecutter.project}}/mapserver}/data/Readme.txt +3 -3
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/demo.map.tmpl +224 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/mapserver.conf +15 -0
- c2cgeoportal_geoportal/scaffolds/create/{mapserver/mapserver.map.tmpl_tmpl → {{cookiecutter.project}}/mapserver/mapserver.map.tmpl} +9 -18
- c2cgeoportal_geoportal/scaffolds/create/{print/print-apps/+package+ → {{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}}/A3_Landscape.jrxml +13 -8
- c2cgeoportal_geoportal/scaffolds/create/{print/print-apps/+package+ → {{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}}/A3_Portrait.jrxml +13 -8
- c2cgeoportal_geoportal/scaffolds/create/{print/print-apps/+package+ → {{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}}/A4_Landscape.jrxml +13 -8
- c2cgeoportal_geoportal/scaffolds/create/{print/print-apps/+package+ → {{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}}/A4_Portrait.jrxml +13 -8
- c2cgeoportal_geoportal/scaffolds/create/{print/print-apps/+package+ → {{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}}/config.yaml.tmpl +11 -4
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/localisation.properties +4 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/localisation_fr.properties +4 -0
- c2cgeoportal_geoportal/scaffolds/create/{project.yaml_tmpl → {{cookiecutter.project}}/project.yaml} +6 -6
- c2cgeoportal_geoportal/scaffolds/create/{pyproject.toml → {{cookiecutter.project}}/pyproject.toml} +4 -0
- c2cgeoportal_geoportal/scaffolds/create/{qgisserver/pg_service.conf.tmpl_tmpl → {{cookiecutter.project}}/qgisserver/pg_service.conf.tmpl} +2 -2
- c2cgeoportal_geoportal/scaffolds/create/{run_alembic.sh → {{cookiecutter.project}}/run_alembic.sh} +3 -5
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/db-backup +110 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/db-restore +114 -0
- c2cgeoportal_geoportal/scaffolds/create/{setup.cfg_tmpl → {{cookiecutter.project}}/setup.cfg} +1 -1
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/spell-ignore-words.txt +5 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tests/__init__.py +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tests/test_app.py +38 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tilegeneration/config.yaml.tmpl +195 -0
- c2cgeoportal_geoportal/scaffolds/update/cookiecutter.json +18 -0
- c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/.upgrade.yaml +61 -0
- c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/CONST_CHANGELOG.txt +273 -0
- c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/CONST_create_template/tests/test_testapp.py +48 -0
- c2cgeoportal_geoportal/scaffolds/update/{geoportal → {{cookiecutter.project}}/geoportal}/CONST_config-schema.yaml +64 -17
- c2cgeoportal_geoportal/scaffolds/update/{geoportal/CONST_vars.yaml_tmpl → {{cookiecutter.project}}/geoportal/CONST_vars.yaml} +396 -19
- c2cgeoportal_geoportal/scripts/__init__.py +16 -30
- c2cgeoportal_geoportal/scripts/c2cupgrade.py +272 -234
- c2cgeoportal_geoportal/scripts/create_demo_theme.py +3 -6
- c2cgeoportal_geoportal/scripts/manage_users.py +34 -39
- c2cgeoportal_geoportal/scripts/pcreate.py +310 -0
- c2cgeoportal_geoportal/scripts/theme2fts.py +128 -24
- c2cgeoportal_geoportal/scripts/urllogin.py +19 -11
- c2cgeoportal_geoportal/templates/login.html +88 -84
- c2cgeoportal_geoportal/templates/notlogin.html +59 -59
- c2cgeoportal_geoportal/templates/testi18n.html +6 -8
- c2cgeoportal_geoportal/views/__init__.py +23 -6
- c2cgeoportal_geoportal/views/dev.py +9 -7
- c2cgeoportal_geoportal/views/dynamic.py +56 -19
- c2cgeoportal_geoportal/views/entry.py +85 -24
- c2cgeoportal_geoportal/views/fulltextsearch.py +29 -23
- c2cgeoportal_geoportal/views/geometry_processing.py +17 -9
- c2cgeoportal_geoportal/views/i18n.py +91 -9
- c2cgeoportal_geoportal/views/layers.py +166 -133
- c2cgeoportal_geoportal/views/login.py +161 -93
- c2cgeoportal_geoportal/views/mapserverproxy.py +47 -31
- c2cgeoportal_geoportal/views/memory.py +12 -12
- c2cgeoportal_geoportal/views/ogcproxy.py +52 -30
- c2cgeoportal_geoportal/views/pdfreport.py +30 -26
- c2cgeoportal_geoportal/views/printproxy.py +60 -52
- c2cgeoportal_geoportal/views/profile.py +24 -23
- c2cgeoportal_geoportal/views/proxy.py +88 -72
- c2cgeoportal_geoportal/views/raster.py +37 -26
- c2cgeoportal_geoportal/views/resourceproxy.py +13 -11
- c2cgeoportal_geoportal/views/shortener.py +27 -25
- c2cgeoportal_geoportal/views/theme.py +472 -332
- c2cgeoportal_geoportal/views/tinyowsproxy.py +42 -44
- c2cgeoportal_geoportal/views/vector_tiles.py +80 -0
- {c2cgeoportal_geoportal-2.6.0.dist-info → c2cgeoportal_geoportal-2.8.1.180.dist-info}/METADATA +19 -8
- c2cgeoportal_geoportal-2.8.1.180.dist-info/RECORD +191 -0
- {c2cgeoportal_geoportal-2.6.0.dist-info → c2cgeoportal_geoportal-2.8.1.180.dist-info}/WHEEL +1 -1
- {c2cgeoportal_geoportal-2.6.0.dist-info → c2cgeoportal_geoportal-2.8.1.180.dist-info}/entry_points.txt +3 -0
- tests/__init__.py +10 -5
- tests/test_cachebuster.py +3 -5
- tests/test_caching.py +18 -26
- tests/test_checker.py +1 -3
- tests/test_decimaljson.py +5 -5
- tests/test_headerstween.py +1 -3
- tests/test_i18n.py +2 -2
- tests/test_init.py +16 -20
- tests/test_locale_negociator.py +4 -6
- tests/test_mapserverproxy_route_predicate.py +1 -4
- tests/test_raster.py +15 -17
- tests/test_wmstparsing.py +10 -12
- tests/xmlstr.py +1 -3
- c2cgeoportal_geoportal/scaffolds/__init__.py +0 -227
- c2cgeoportal_geoportal/scaffolds/create/+dot+dockerignore_tmpl +0 -12
- c2cgeoportal_geoportal/scaffolds/create/+dot+github/workflows/main.yaml_tmpl +0 -89
- c2cgeoportal_geoportal/scaffolds/create/+dot+github/workflows/rebuild.yaml_tmpl +0 -78
- c2cgeoportal_geoportal/scaffolds/create/+dot+gitignore_tmpl +0 -16
- c2cgeoportal_geoportal/scaffolds/create/Dockerfile_tmpl +0 -67
- c2cgeoportal_geoportal/scaffolds/create/Makefile +0 -3
- c2cgeoportal_geoportal/scaffolds/create/build_tmpl +0 -167
- c2cgeoportal_geoportal/scaffolds/create/ci/config.yaml_tmpl +0 -23
- c2cgeoportal_geoportal/scaffolds/create/ci/requirements.txt +0 -1
- c2cgeoportal_geoportal/scaffolds/create/ci/trigger +0 -68
- c2cgeoportal_geoportal/scaffolds/create/docker-compose.override.sample.yaml +0 -54
- c2cgeoportal_geoportal/scaffolds/create/env.default_tmpl +0 -67
- c2cgeoportal_geoportal/scaffolds/create/env.project_tmpl +0 -48
- c2cgeoportal_geoportal/scaffolds/create/geoportal/+dot+dockerignore_tmpl +0 -6
- c2cgeoportal_geoportal/scaffolds/create/geoportal/+dot+eslintrc_tmpl +0 -15
- c2cgeoportal_geoportal/scaffolds/create/geoportal/+package+_geoportal/__init__.py_tmpl +0 -58
- c2cgeoportal_geoportal/scaffolds/create/geoportal/+package+_geoportal/models.py_tmpl +0 -10
- c2cgeoportal_geoportal/scaffolds/create/geoportal/+package+_geoportal/static/robot.txt +0 -3
- c2cgeoportal_geoportal/scaffolds/create/geoportal/production.ini_tmpl +0 -106
- c2cgeoportal_geoportal/scaffolds/create/geoportal/tools/extract-messages.js +0 -39
- c2cgeoportal_geoportal/scaffolds/create/geoportal/tsconfig.json_tmpl +0 -9
- c2cgeoportal_geoportal/scaffolds/create/geoportal/webpack.api.js_tmpl +0 -72
- c2cgeoportal_geoportal/scaffolds/create/mapserver/demo.map.tmpl_tmpl +0 -262
- c2cgeoportal_geoportal/scaffolds/create/mapserver/tinyows.xml +0 -36
- c2cgeoportal_geoportal/scaffolds/create/print/print-apps/+package+/config.yaml +0 -168
- c2cgeoportal_geoportal/scaffolds/create/qgisserver/geomapfish.yaml.tmpl_tmpl +0 -16
- c2cgeoportal_geoportal/scaffolds/create/spell-ignore-words.txt +0 -1
- c2cgeoportal_geoportal/scaffolds/create/tilegeneration/config.yaml.tmpl_tmpl +0 -185
- c2cgeoportal_geoportal/scaffolds/create/yamllint.yaml +0 -11
- c2cgeoportal_geoportal/scaffolds/update/+dot+upgrade.yaml_tmpl +0 -181
- c2cgeoportal_geoportal/scaffolds/update/CONST_CHANGELOG.txt_tmpl +0 -454
- c2cgeoportal_geoportal/templates/dynamic.js +0 -21
- c2cgeoportal_geoportal-2.6.0.dist-info/RECORD +0 -173
- /c2cgeoportal_geoportal/{scaffolds/create/geoportal/+package+_geoportal/static/css/desktop.css → py.typed} +0 -0
- /c2cgeoportal_geoportal/scaffolds/{create/geoportal/Makefile_tmpl → advance_create/{{cookiecutter.project}}/geoportal/Makefile} +0 -0
- /c2cgeoportal_geoportal/scaffolds/{create → advance_create/{{cookiecutter.project}}}/geoportal/alembic.ini +0 -0
- /c2cgeoportal_geoportal/scaffolds/{create → advance_create/{{cookiecutter.project}}}/geoportal/language_mapping +0 -0
- /c2cgeoportal_geoportal/scaffolds/{create → advance_create/{{cookiecutter.project}}}/geoportal/lingua-server.cfg +0 -0
- /c2cgeoportal_geoportal/scaffolds/{create → advance_create/{{cookiecutter.project}}}/geoportal/requirements.txt +0 -0
- /c2cgeoportal_geoportal/scaffolds/{create/geoportal/+package+_geoportal → advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal}/views/__init__.py +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{geoportal/+package+_geoportal/locale/en/LC_MESSAGES/+package+_geoportal-client.po → {{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/locale/en/LC_MESSAGES/{{cookiecutter.package}}_geoportal-client.po} +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{geoportal/+package+_geoportal/static/css/iframe_api.css → {{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/css/desktop.css} +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{geoportal/+package+_geoportal/static/css/mobile.css → {{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/css/iframe_api.css} +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{geoportal/+package+_geoportal → {{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal}/static/images/banner_left.png +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{geoportal/+package+_geoportal → {{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal}/static/images/banner_right.png +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{geoportal/+package+_geoportal → {{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal}/static/images/blank.png +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{geoportal/+package+_geoportal → {{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal}/static/images/markers/marker-blue.png +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{geoportal/+package+_geoportal → {{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal}/static/images/markers/marker-gold.png +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{geoportal/+package+_geoportal → {{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal}/static/images/markers/marker-green.png +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{geoportal/+package+_geoportal → {{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal}/static/images/markers/marker.png +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{geoportal/+package+_geoportal → {{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal}/static/robot.txt.tmpl +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{mapserver → {{cookiecutter.project}}/mapserver}/data/TM_EUROPE_BORDERS-0.3.sql +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{mapserver → {{cookiecutter.project}}/mapserver}/fonts/Arial.ttf +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{mapserver → {{cookiecutter.project}}/mapserver}/fonts/Arialbd.ttf +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{mapserver → {{cookiecutter.project}}/mapserver}/fonts/Arialbi.ttf +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{mapserver → {{cookiecutter.project}}/mapserver}/fonts/Ariali.ttf +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{mapserver → {{cookiecutter.project}}/mapserver}/fonts/NotoSans-Bold.ttf +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{mapserver → {{cookiecutter.project}}/mapserver}/fonts/NotoSans-BoldItalic.ttf +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{mapserver → {{cookiecutter.project}}/mapserver}/fonts/NotoSans-Italic.ttf +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{mapserver → {{cookiecutter.project}}/mapserver}/fonts/NotoSans-Regular.ttf +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{mapserver → {{cookiecutter.project}}/mapserver}/fonts/Verdana.ttf +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{mapserver → {{cookiecutter.project}}/mapserver}/fonts/Verdanab.ttf +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{mapserver → {{cookiecutter.project}}/mapserver}/fonts/Verdanai.ttf +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{mapserver → {{cookiecutter.project}}/mapserver}/fonts/Verdanaz.ttf +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{mapserver → {{cookiecutter.project}}/mapserver}/fonts.conf +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{mapserver → {{cookiecutter.project}}/mapserver}/tinyows.xml.tmpl +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{print/print-apps/+package+ → {{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}}/legend.jrxml +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{print/print-apps/+package+ → {{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}}/logo.png +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{print/print-apps/+package+ → {{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}}/north.svg +0 -0
- /c2cgeoportal_geoportal/scaffolds/create/{print/print-apps/+package+ → {{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}}/results.jrxml +0 -0
- {c2cgeoportal_geoportal-2.6.0.dist-info → c2cgeoportal_geoportal-2.8.1.180.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,4 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
# Copyright (c) 2011-2021, Camptocamp SA
|
1
|
+
# Copyright (c) 2011-2024, Camptocamp SA
|
4
2
|
# All rights reserved.
|
5
3
|
|
6
4
|
# Redistribution and use in source and binary forms, with or without
|
@@ -27,33 +25,38 @@
|
|
27
25
|
# of the authors and should not be interpreted as representing official policies,
|
28
26
|
# either expressed or implied, of the FreeBSD Project.
|
29
27
|
|
30
|
-
# pylint: disable=no-member
|
31
|
-
|
32
28
|
|
33
29
|
import asyncio
|
34
30
|
import gc
|
35
31
|
import logging
|
32
|
+
import os
|
36
33
|
import re
|
37
34
|
import sys
|
38
35
|
import time
|
39
|
-
import urllib.parse
|
40
36
|
from collections import Counter
|
41
37
|
from math import sqrt
|
42
|
-
from typing import Any, Dict, List, Set, Union, cast
|
38
|
+
from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast
|
43
39
|
|
40
|
+
import dogpile.cache.api
|
41
|
+
import pyramid.httpexceptions
|
42
|
+
import pyramid.request
|
44
43
|
import requests
|
44
|
+
import sqlalchemy
|
45
|
+
import sqlalchemy.orm.query
|
45
46
|
from c2cwsgiutils.auth import auth_view
|
46
47
|
from defusedxml import lxml
|
48
|
+
from lxml import etree # nosec
|
47
49
|
from owslib.wms import WebMapService
|
48
50
|
from pyramid.view import view_config
|
49
51
|
from sqlalchemy.orm import subqueryload
|
50
52
|
from sqlalchemy.orm.exc import NoResultFound
|
51
53
|
|
52
54
|
from c2cgeoportal_commons import models
|
53
|
-
from c2cgeoportal_commons.lib.url import
|
54
|
-
from c2cgeoportal_commons.models import main
|
55
|
+
from c2cgeoportal_commons.lib.url import Url, get_url2
|
56
|
+
from c2cgeoportal_commons.models import cache_invalidate_cb, main
|
55
57
|
from c2cgeoportal_geoportal.lib import get_roles_id, get_typed, get_types_map, is_intranet
|
56
|
-
from c2cgeoportal_geoportal.lib.caching import
|
58
|
+
from c2cgeoportal_geoportal.lib.caching import get_region
|
59
|
+
from c2cgeoportal_geoportal.lib.common_headers import Cache, set_common_headers
|
57
60
|
from c2cgeoportal_geoportal.lib.functionality import get_mapserver_substitution_params
|
58
61
|
from c2cgeoportal_geoportal.lib.layers import (
|
59
62
|
get_private_layers,
|
@@ -61,30 +64,42 @@ from c2cgeoportal_geoportal.lib.layers import (
|
|
61
64
|
get_protected_layers_query,
|
62
65
|
)
|
63
66
|
from c2cgeoportal_geoportal.lib.wmstparsing import TimeInformation, parse_extent
|
64
|
-
from c2cgeoportal_geoportal.views.layers import
|
67
|
+
from c2cgeoportal_geoportal.views.layers import get_layer_metadata
|
65
68
|
|
66
69
|
LOG = logging.getLogger(__name__)
|
67
70
|
CACHE_REGION = get_region("std")
|
71
|
+
CACHE_OGC_SERVER_REGION = get_region("ogc-server")
|
72
|
+
TIMEOUT = int(os.environ.get("C2CGEOPORTAL_THEME_TIMEOUT", "300"))
|
73
|
+
|
74
|
+
Metadata = Union[str, int, float, bool, List[Any], Dict[str, Any]]
|
75
|
+
|
68
76
|
|
77
|
+
def get_http_cached(
|
78
|
+
http_options: Dict[str, Any], url: str, headers: Dict[str, str], cache: bool = True
|
79
|
+
) -> Tuple[bytes, str]:
|
80
|
+
"""Get the content of the URL with a cash (dogpile)."""
|
69
81
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
response
|
74
|
-
LOG.info("Get
|
75
|
-
return response
|
82
|
+
@CACHE_OGC_SERVER_REGION.cache_on_arguments() # type: ignore
|
83
|
+
def do_get_http_cached(url: str) -> Tuple[bytes, str]:
|
84
|
+
response = requests.get(url, headers=headers, timeout=TIMEOUT, **http_options)
|
85
|
+
response.raise_for_status()
|
86
|
+
LOG.info("Get URL '%s' in %.1fs.", url, response.elapsed.total_seconds())
|
87
|
+
return response.content, response.headers.get("Content-Type", "")
|
76
88
|
|
77
|
-
|
89
|
+
if cache:
|
90
|
+
return do_get_http_cached(url) # type: ignore
|
91
|
+
return do_get_http_cached.refresh(url) # type: ignore
|
78
92
|
|
79
93
|
|
80
94
|
class DimensionInformation:
|
95
|
+
"""Used to collect the dimensions information."""
|
81
96
|
|
82
97
|
URL_PART_RE = re.compile(r"[a-zA-Z0-9_\-\+~\.]*$")
|
83
98
|
|
84
99
|
def __init__(self) -> None:
|
85
100
|
self._dimensions: Dict[str, str] = {}
|
86
101
|
|
87
|
-
def merge(self, layer, layer_node, mixed):
|
102
|
+
def merge(self, layer: main.Layer, layer_node: Dict[str, Any], mixed: bool) -> Set[str]:
|
88
103
|
errors = set()
|
89
104
|
|
90
105
|
dimensions: Dict[str, str] = {}
|
@@ -96,14 +111,11 @@ class DimensionInformation:
|
|
96
111
|
and not self.URL_PART_RE.match(dimension.value)
|
97
112
|
):
|
98
113
|
errors.add(
|
99
|
-
"The layer '{}' has an unsupported dimension value
|
100
|
-
|
101
|
-
)
|
114
|
+
f"The layer '{layer.name}' has an unsupported dimension value "
|
115
|
+
f"'{dimension.value}' ('{dimension.name}')."
|
102
116
|
)
|
103
117
|
elif dimension.name in dimensions: # pragma: nocover
|
104
|
-
errors.add(
|
105
|
-
"The layer '{}' has a duplicated dimension name '{}'.".format(layer.name, dimension.name)
|
106
|
-
)
|
118
|
+
errors.add(f"The layer '{layer.name}' has a duplicated dimension name '{dimension.name}'.")
|
107
119
|
else:
|
108
120
|
if dimension.field:
|
109
121
|
dimensions_filters[dimension.name] = {"field": dimension.field, "value": dimension.value}
|
@@ -120,20 +132,22 @@ class DimensionInformation:
|
|
120
132
|
self._dimensions[name] = value
|
121
133
|
elif self._dimensions[name] != value and value is not None:
|
122
134
|
errors.add(
|
123
|
-
"The layer '{}' has a wrong dimension value '{}' for '{}', "
|
124
|
-
"expected '{}' or empty."
|
135
|
+
f"The layer '{layer.name}' has a wrong dimension value '{value}' for '{name}', "
|
136
|
+
f"expected '{self._dimensions[name]}' or empty."
|
125
137
|
)
|
126
138
|
return errors
|
127
139
|
|
128
|
-
def get_dimensions(self):
|
140
|
+
def get_dimensions(self) -> Dict[str, str]:
|
129
141
|
return self._dimensions
|
130
142
|
|
131
143
|
|
132
144
|
class Theme:
|
133
|
-
|
145
|
+
"""All the views concerning the themes."""
|
146
|
+
|
147
|
+
def __init__(self, request: pyramid.request.Request):
|
134
148
|
self.request = request
|
135
149
|
self.settings = request.registry.settings
|
136
|
-
self.http_options = self.
|
150
|
+
self.http_options = self.settings.get("http_options", {})
|
137
151
|
self.metadata_type = get_types_map(
|
138
152
|
self.settings.get("admin_interface", {}).get("available_metadata", [])
|
139
153
|
)
|
@@ -145,8 +159,10 @@ class Theme:
|
|
145
159
|
self._layergroup_cache = None
|
146
160
|
self._themes_cache = None
|
147
161
|
|
148
|
-
def _get_metadata(
|
149
|
-
|
162
|
+
def _get_metadata(
|
163
|
+
self, item: main.TreeItem, metadata: str, errors: Set[str]
|
164
|
+
) -> Union[None, str, int, float, bool, List[Any], Dict[str, Any]]:
|
165
|
+
metadatas = item.get_metadata(metadata)
|
150
166
|
return (
|
151
167
|
None
|
152
168
|
if not metadatas
|
@@ -155,8 +171,9 @@ class Theme:
|
|
155
171
|
)
|
156
172
|
)
|
157
173
|
|
158
|
-
def
|
159
|
-
metadatas = {}
|
174
|
+
def _get_metadata_list(self, item: main.TreeItem, errors: Set[str]) -> Dict[str, Metadata]:
|
175
|
+
metadatas: Dict[str, Metadata] = {}
|
176
|
+
metadata: main.Metadata
|
160
177
|
for metadata in item.metadatas:
|
161
178
|
value = get_typed(metadata.name, metadata.value, self.metadata_type, self.request, errors)
|
162
179
|
if value is not None:
|
@@ -164,24 +181,27 @@ class Theme:
|
|
164
181
|
|
165
182
|
return metadatas
|
166
183
|
|
167
|
-
async def _wms_getcap(
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
return None, errors
|
184
|
+
async def _wms_getcap(
|
185
|
+
self, ogc_server: main.OGCServer, preload: bool = False, cache: bool = True
|
186
|
+
) -> Tuple[Optional[Dict[str, Dict[str, Any]]], Set[str]]:
|
187
|
+
LOG.debug("Get the WMS Capabilities of '%s', preload: %s, cache: %s", ogc_server.name, preload, cache)
|
172
188
|
|
173
|
-
@
|
174
|
-
def build_web_map_service(ogc_server_id):
|
189
|
+
@CACHE_OGC_SERVER_REGION.cache_on_arguments() # type: ignore
|
190
|
+
def build_web_map_service(ogc_server_id: int) -> Tuple[Optional[Dict[str, Dict[str, Any]]], Set[str]]:
|
175
191
|
del ogc_server_id # Just for cache
|
176
192
|
|
177
|
-
|
193
|
+
if url is None:
|
194
|
+
raise RuntimeError("URL is None")
|
195
|
+
|
196
|
+
version = url.query.get("VERSION", "1.1.1")
|
178
197
|
layers = {}
|
179
198
|
try:
|
180
199
|
wms = WebMapService(None, xml=content, version=version)
|
181
|
-
except Exception as e:
|
200
|
+
except Exception as e:
|
182
201
|
error = (
|
183
|
-
"WARNING! an error '{}' occurred while trying to read the mapfile and
|
184
|
-
"
|
202
|
+
f"WARNING! an error '{e!s}' occurred while trying to read the mapfile and "
|
203
|
+
"recover the themes."
|
204
|
+
f"\nURL: {url}\n{content.decode() if content else None}"
|
185
205
|
)
|
186
206
|
LOG.error(error, exc_info=True)
|
187
207
|
return None, {error}
|
@@ -191,8 +211,8 @@ class Theme:
|
|
191
211
|
resolution = self._get_layer_resolution_hint(wms_layer)
|
192
212
|
info = {
|
193
213
|
"name": wms_layer.name,
|
194
|
-
"minResolutionHint": float("{:0.2f}"
|
195
|
-
"maxResolutionHint": float("{:0.2f}"
|
214
|
+
"minResolutionHint": float(f"{resolution[0]:0.2f}"),
|
215
|
+
"maxResolutionHint": float(f"{resolution[1]:0.2f}"),
|
196
216
|
}
|
197
217
|
if hasattr(wms_layer, "queryable"):
|
198
218
|
info["queryable"] = wms_layer.queryable == 1
|
@@ -205,16 +225,34 @@ class Theme:
|
|
205
225
|
}
|
206
226
|
|
207
227
|
del wms
|
208
|
-
LOG.debug("Run garbage collection: %s", ", ".join([str(gc.collect(n)) for n in range(3)]))
|
209
228
|
|
210
229
|
return {"layers": layers}, set()
|
211
230
|
|
212
|
-
|
231
|
+
if cache:
|
232
|
+
result = build_web_map_service.get(ogc_server.id)
|
233
|
+
if result != dogpile.cache.api.NO_VALUE:
|
234
|
+
return result # type: ignore
|
213
235
|
|
214
|
-
|
236
|
+
try:
|
237
|
+
url, content, errors = await self._wms_getcap_cached(ogc_server, cache=cache)
|
238
|
+
except requests.exceptions.RequestException as exception:
|
239
|
+
error = (
|
240
|
+
f"Unable to get the WMS Capabilities for OGC server '{ogc_server.name}', "
|
241
|
+
f"return the error: {exception.response.status_code} {exception.response.reason}"
|
242
|
+
)
|
243
|
+
LOG.exception(error)
|
244
|
+
return None, {error}
|
245
|
+
if errors or preload:
|
246
|
+
return None, errors
|
247
|
+
|
248
|
+
return build_web_map_service.refresh(ogc_server.id) # type: ignore
|
249
|
+
|
250
|
+
async def _wms_getcap_cached(
|
251
|
+
self, ogc_server: main.OGCServer, cache: bool = True
|
252
|
+
) -> Tuple[Optional[Url], Optional[bytes], Set[str]]:
|
215
253
|
errors: Set[str] = set()
|
216
|
-
url = get_url2("The OGC server '{}'"
|
217
|
-
if errors or url is None:
|
254
|
+
url = get_url2(f"The OGC server '{ogc_server.name}'", ogc_server.url, self.request, errors)
|
255
|
+
if errors or url is None:
|
218
256
|
return url, None, errors
|
219
257
|
|
220
258
|
# Add functionality params
|
@@ -222,70 +260,52 @@ class Theme:
|
|
222
260
|
ogc_server.auth == main.OGCSERVER_AUTH_STANDARD
|
223
261
|
and ogc_server.type == main.OGCSERVER_TYPE_MAPSERVER
|
224
262
|
):
|
225
|
-
url
|
263
|
+
url.add_query(get_mapserver_substitution_params(self.request))
|
226
264
|
|
227
|
-
url
|
228
|
-
url,
|
265
|
+
url.add_query(
|
229
266
|
{
|
230
267
|
"SERVICE": "WMS",
|
231
268
|
"VERSION": "1.1.1",
|
232
269
|
"REQUEST": "GetCapabilities",
|
233
|
-
"
|
270
|
+
"ROLE_IDS": "0",
|
234
271
|
"USER_ID": "0",
|
235
272
|
},
|
236
273
|
)
|
237
274
|
|
238
275
|
LOG.debug("Get WMS GetCapabilities for URL: %s", url)
|
239
276
|
|
240
|
-
|
241
|
-
headers = dict(self.request.headers)
|
277
|
+
headers = {}
|
242
278
|
|
243
279
|
# Add headers for Geoserver
|
244
280
|
if ogc_server.auth == main.OGCSERVER_AUTH_GEOSERVER:
|
245
281
|
headers["sec-username"] = "root"
|
246
282
|
headers["sec-roles"] = "root"
|
247
283
|
|
248
|
-
if urllib.parse.urlsplit(url).hostname != "localhost" and "Host" in headers: # pragma: no cover
|
249
|
-
headers.pop("Host")
|
250
|
-
|
251
284
|
try:
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
except Exception: # pragma: no cover
|
256
|
-
error = "Unable to GetCapabilities from URL {}".format(url)
|
285
|
+
content, content_type = get_http_cached(self.http_options, url.url(), headers, cache=cache)
|
286
|
+
except Exception:
|
287
|
+
error = f"Unable to GetCapabilities from URL {url}"
|
257
288
|
errors.add(error)
|
258
289
|
LOG.error(error, exc_info=True)
|
259
290
|
return url, None, errors
|
260
291
|
|
261
|
-
if not response.ok: # pragma: no cover
|
262
|
-
error = "GetCapabilities from URL {} return the error: {:d} {}".format(
|
263
|
-
url, response.status_code, response.reason
|
264
|
-
)
|
265
|
-
errors.add(error)
|
266
|
-
LOG.error(error)
|
267
|
-
return url, None, errors
|
268
|
-
|
269
292
|
# With wms 1.3 it returns text/xml also in case of error :-(
|
270
|
-
if
|
293
|
+
if content_type.split(";")[0].strip() not in [
|
271
294
|
"application/vnd.ogc.wms_xml",
|
272
295
|
"text/xml",
|
273
296
|
]:
|
274
|
-
error =
|
275
|
-
url
|
297
|
+
error = (
|
298
|
+
f"GetCapabilities from URL '{url}' returns a wrong Content-Type: {content_type}\n"
|
299
|
+
f"{content.decode()}"
|
276
300
|
)
|
277
301
|
errors.add(error)
|
278
302
|
LOG.error(error)
|
279
303
|
return url, None, errors
|
280
304
|
|
281
|
-
return url,
|
282
|
-
|
283
|
-
def _create_layer_query(self, interface):
|
284
|
-
"""
|
285
|
-
Create an SQLAlchemy query for Layer and for the role
|
286
|
-
identified to by ``role_id``.
|
287
|
-
"""
|
305
|
+
return url, content, errors
|
288
306
|
|
307
|
+
def _create_layer_query(self, interface: str) -> sqlalchemy.orm.query.Query:
|
308
|
+
"""Create an SQLAlchemy query for Layer and for the role identified to by ``role_id``."""
|
289
309
|
query = models.DBSession.query(main.Layer.name).filter(main.Layer.public.is_(True))
|
290
310
|
|
291
311
|
if interface is not None:
|
@@ -305,7 +325,7 @@ class Theme:
|
|
305
325
|
|
306
326
|
return query
|
307
327
|
|
308
|
-
def _get_layer_metadata_urls(self, layer):
|
328
|
+
def _get_layer_metadata_urls(self, layer: main.Layer) -> List[str]:
|
309
329
|
metadata_urls: List[str] = []
|
310
330
|
if layer.metadataUrls:
|
311
331
|
metadata_urls = layer.metadataUrls
|
@@ -313,7 +333,7 @@ class Theme:
|
|
313
333
|
metadata_urls.extend(self._get_layer_metadata_urls(child_layer))
|
314
334
|
return metadata_urls
|
315
335
|
|
316
|
-
def _get_layer_resolution_hint_raw(self, layer):
|
336
|
+
def _get_layer_resolution_hint_raw(self, layer: main.Layer) -> Tuple[Optional[float], Optional[float]]:
|
317
337
|
resolution_hint_min = None
|
318
338
|
resolution_hint_max = None
|
319
339
|
if layer.scaleHint:
|
@@ -345,40 +365,45 @@ class Theme:
|
|
345
365
|
|
346
366
|
return (resolution_hint_min, resolution_hint_max)
|
347
367
|
|
348
|
-
def _get_layer_resolution_hint(self, layer):
|
368
|
+
def _get_layer_resolution_hint(self, layer: main.Layer) -> Tuple[float, float]:
|
349
369
|
resolution_hint_min, resolution_hint_max = self._get_layer_resolution_hint_raw(layer)
|
350
370
|
return (
|
351
371
|
0.0 if resolution_hint_min is None else resolution_hint_min,
|
352
372
|
999999999 if resolution_hint_max is None else resolution_hint_max,
|
353
373
|
)
|
354
374
|
|
355
|
-
def _layer(
|
375
|
+
async def _layer(
|
376
|
+
self,
|
377
|
+
layer: main.Layer,
|
378
|
+
time_: Optional[TimeInformation] = None,
|
379
|
+
dim: Optional[DimensionInformation] = None,
|
380
|
+
mixed: bool = True,
|
381
|
+
) -> Tuple[Optional[Dict[str, Any]], Set[str]]:
|
356
382
|
errors: Set[str] = set()
|
357
|
-
layer_info = {"id": layer.id, "name": layer.name, "metadata": self.
|
358
|
-
if re.search("[/?#]", layer.name):
|
359
|
-
errors.add("The layer has an unsupported name '{}'."
|
360
|
-
if isinstance(layer, main.LayerWMS) and re.search("[/?#]", layer.layer): # pragma: no cover
|
361
|
-
errors.add("The layer has an unsupported layers '{}'.".format(layer.layer))
|
383
|
+
layer_info = {"id": layer.id, "name": layer.name, "metadata": self._get_metadata_list(layer, errors)}
|
384
|
+
if re.search("[/?#]", layer.name):
|
385
|
+
errors.add(f"The layer has an unsupported name '{layer.name}'.")
|
362
386
|
if layer.geo_table:
|
363
387
|
errors |= self._fill_editable(layer_info, layer)
|
364
388
|
if mixed:
|
365
389
|
assert time_ is None
|
366
390
|
time_ = TimeInformation()
|
367
391
|
assert time_ is not None
|
392
|
+
assert dim is not None
|
368
393
|
|
369
394
|
errors |= dim.merge(layer, layer_info, mixed)
|
370
395
|
|
371
396
|
if isinstance(layer, main.LayerWMS):
|
372
|
-
wms, wms_errors = self._wms_layers(layer.ogc_server)
|
397
|
+
wms, wms_errors = await self._wms_layers(layer.ogc_server)
|
373
398
|
errors |= wms_errors
|
374
399
|
if wms is None:
|
375
400
|
return None if errors else layer_info, errors
|
376
401
|
if layer.layer is None or layer.layer == "":
|
377
|
-
errors.add("The layer '{}' do not have any layers"
|
402
|
+
errors.add(f"The layer '{layer.name}' do not have any layers")
|
378
403
|
return None, errors
|
379
404
|
layer_info["type"] = "WMS"
|
380
405
|
layer_info["layers"] = layer.layer
|
381
|
-
self._fill_wms(layer_info, layer, errors, mixed=mixed)
|
406
|
+
await self._fill_wms(layer_info, layer, errors, mixed=mixed)
|
382
407
|
errors |= self._merge_time(time_, layer_info, layer, wms)
|
383
408
|
|
384
409
|
elif isinstance(layer, main.LayerWMTS):
|
@@ -387,16 +412,18 @@ class Theme:
|
|
387
412
|
|
388
413
|
elif isinstance(layer, main.LayerVectorTiles):
|
389
414
|
layer_info["type"] = "VectorTiles"
|
390
|
-
self._vectortiles_layers(layer_info, layer)
|
415
|
+
self._vectortiles_layers(layer_info, layer, errors)
|
391
416
|
|
392
417
|
return None if errors else layer_info, errors
|
393
418
|
|
394
419
|
@staticmethod
|
395
|
-
def _merge_time(
|
420
|
+
def _merge_time(
|
421
|
+
time_: TimeInformation, layer_theme: Dict[str, Any], layer: main.Layer, wms: Dict[str, Dict[str, Any]]
|
422
|
+
) -> Set[str]:
|
396
423
|
errors = set()
|
397
424
|
wmslayer = layer.layer
|
398
425
|
|
399
|
-
def merge_time(wms_layer_obj):
|
426
|
+
def merge_time(wms_layer_obj: Dict[str, Any]) -> None:
|
400
427
|
extent = parse_extent(wms_layer_obj["timepositions"], wms_layer_obj["defaulttimeposition"])
|
401
428
|
time_.merge(layer_theme, extent, layer.time_mode, layer.time_widget)
|
402
429
|
|
@@ -420,17 +447,15 @@ class Theme:
|
|
420
447
|
|
421
448
|
if not has_time:
|
422
449
|
errors.add(
|
423
|
-
"Error: time layer '{}' has no time information in capabilities"
|
424
|
-
layer.name
|
425
|
-
)
|
450
|
+
f"Error: time layer '{layer.name}' has no time information in capabilities"
|
426
451
|
)
|
427
452
|
|
428
453
|
except ValueError: # pragma no cover
|
429
|
-
errors.add("Error while handling time for layer '{}': {
|
454
|
+
errors.add(f"Error while handling time for layer '{layer.name}': {sys.exc_info()[1]}")
|
430
455
|
|
431
456
|
return errors
|
432
457
|
|
433
|
-
def _fill_editable(self, layer_theme, layer):
|
458
|
+
def _fill_editable(self, layer_theme: Dict[str, Any], layer: main.Layer) -> Set[str]:
|
434
459
|
errors = set()
|
435
460
|
try:
|
436
461
|
if self.request.user:
|
@@ -443,14 +468,19 @@ class Theme:
|
|
443
468
|
.count()
|
444
469
|
)
|
445
470
|
if count > 0:
|
446
|
-
layer_theme["edit_columns"] =
|
471
|
+
layer_theme["edit_columns"] = get_layer_metadata(layer)
|
447
472
|
layer_theme["editable"] = True
|
448
473
|
except Exception as exception:
|
449
474
|
LOG.exception(str(exception))
|
450
475
|
errors.add(str(exception))
|
451
476
|
return errors
|
452
477
|
|
453
|
-
def _fill_child_layer(
|
478
|
+
def _fill_child_layer(
|
479
|
+
self,
|
480
|
+
layer_theme: Dict[str, Any],
|
481
|
+
layer_name: str,
|
482
|
+
wms: Dict[str, Dict[str, Any]],
|
483
|
+
) -> None:
|
454
484
|
wms_layer_obj = wms["layers"][layer_name]
|
455
485
|
if not wms_layer_obj["children"]:
|
456
486
|
layer_theme["childLayers"].append(wms["layers"][layer_name]["info"])
|
@@ -458,14 +488,16 @@ class Theme:
|
|
458
488
|
for child_layer in wms_layer_obj["children"]:
|
459
489
|
self._fill_child_layer(layer_theme, child_layer, wms)
|
460
490
|
|
461
|
-
def _fill_wms(
|
462
|
-
|
491
|
+
async def _fill_wms(
|
492
|
+
self, layer_theme: Dict[str, Any], layer: main.Layer, errors: Set[str], mixed: bool
|
493
|
+
) -> None:
|
494
|
+
wms, wms_errors = await self._wms_layers(layer.ogc_server)
|
463
495
|
errors |= wms_errors
|
464
496
|
if wms is None:
|
465
497
|
return
|
466
498
|
|
467
499
|
layer_theme["imageType"] = layer.ogc_server.image_type
|
468
|
-
if layer.style:
|
500
|
+
if layer.style:
|
469
501
|
layer_theme["style"] = layer.style
|
470
502
|
|
471
503
|
# now look at what is in the WMS capabilities doc
|
@@ -475,9 +507,8 @@ class Theme:
|
|
475
507
|
self._fill_child_layer(layer_theme, layer_name, wms)
|
476
508
|
else:
|
477
509
|
errors.add(
|
478
|
-
"The layer '{}' ({}) is not defined in WMS capabilities
|
479
|
-
|
480
|
-
)
|
510
|
+
f"The layer '{layer_name}' ({layer.name}) is not defined in WMS capabilities "
|
511
|
+
f"from '{layer.ogc_server.name}'"
|
481
512
|
)
|
482
513
|
|
483
514
|
if "minResolutionHint" not in layer_theme:
|
@@ -506,26 +537,9 @@ class Theme:
|
|
506
537
|
if mixed:
|
507
538
|
layer_theme["ogcServer"] = layer.ogc_server.name
|
508
539
|
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
layer_theme["icon"] = add_url_params(
|
513
|
-
url,
|
514
|
-
{
|
515
|
-
"SERVICE": "WMS",
|
516
|
-
"VERSION": "1.1.1",
|
517
|
-
"REQUEST": "GetLegendGraphic",
|
518
|
-
"LAYER": layer.name,
|
519
|
-
"FORMAT": "image/png",
|
520
|
-
"TRANSPARENT": "TRUE",
|
521
|
-
"RULE": layer.legend_rule,
|
522
|
-
},
|
523
|
-
)
|
524
|
-
|
525
|
-
def _fill_wmts(self, layer_theme, layer, errors):
|
526
|
-
layer_theme["url"] = get_url2(
|
527
|
-
"The WMTS layer '{}'".format(layer.name), layer.url, self.request, errors=errors
|
528
|
-
)
|
540
|
+
def _fill_wmts(self, layer_theme: Dict[str, Any], layer: main.Layer, errors: Set[str]) -> None:
|
541
|
+
url = get_url2(f"The WMTS layer '{layer.name}'", layer.url, self.request, errors=errors)
|
542
|
+
layer_theme["url"] = url.url() if url is not None else None
|
529
543
|
|
530
544
|
if layer.style:
|
531
545
|
layer_theme["style"] = layer.style
|
@@ -535,18 +549,18 @@ class Theme:
|
|
535
549
|
layer_theme["layer"] = layer.layer
|
536
550
|
layer_theme["imageType"] = layer.image_type
|
537
551
|
|
538
|
-
|
539
|
-
|
540
|
-
layer_theme["style"] =
|
552
|
+
def _vectortiles_layers(self, layer_theme: Dict[str, Any], layer: main.Layer, errors: Set[str]) -> None:
|
553
|
+
style = get_url2(f"The VectorTiles layer '{layer.name}'", layer.style, self.request, errors=errors)
|
554
|
+
layer_theme["style"] = style.url() if style is not None else None
|
541
555
|
if layer.xyz:
|
542
556
|
layer_theme["xyz"] = layer.xyz
|
543
557
|
|
544
558
|
@staticmethod
|
545
|
-
def _layer_included(tree_item):
|
559
|
+
def _layer_included(tree_item: main.TreeItem) -> bool:
|
546
560
|
return isinstance(tree_item, main.Layer)
|
547
561
|
|
548
|
-
def _get_ogc_servers(self, group, depth):
|
549
|
-
"""
|
562
|
+
def _get_ogc_servers(self, group: main.LayerGroup, depth: int) -> Set[Union[str, bool]]:
|
563
|
+
"""Get unique identifier for each child by recursing on all the children."""
|
550
564
|
|
551
565
|
ogc_servers: Set[Union[str, bool]] = set()
|
552
566
|
|
@@ -569,23 +583,23 @@ class Theme:
|
|
569
583
|
return ogc_servers
|
570
584
|
|
571
585
|
@staticmethod
|
572
|
-
def is_mixed(ogc_servers):
|
586
|
+
def is_mixed(ogc_servers: List[Union[str, bool]]) -> bool:
|
573
587
|
return len(ogc_servers) != 1 or ogc_servers[0] is False
|
574
588
|
|
575
|
-
def _group(
|
589
|
+
async def _group(
|
576
590
|
self,
|
577
|
-
path,
|
578
|
-
group,
|
579
|
-
layers,
|
580
|
-
depth=1,
|
581
|
-
min_levels=1,
|
582
|
-
mixed=True,
|
583
|
-
time_=None,
|
584
|
-
dim=None,
|
585
|
-
wms_layers=None,
|
586
|
-
layers_name=None,
|
587
|
-
**kwargs,
|
588
|
-
):
|
591
|
+
path: str,
|
592
|
+
group: main.LayerGroup,
|
593
|
+
layers: List[str],
|
594
|
+
depth: int = 1,
|
595
|
+
min_levels: int = 1,
|
596
|
+
mixed: bool = True,
|
597
|
+
time_: Optional[TimeInformation] = None,
|
598
|
+
dim: Optional[DimensionInformation] = None,
|
599
|
+
wms_layers: Optional[List[str]] = None,
|
600
|
+
layers_name: Optional[List[str]] = None,
|
601
|
+
**kwargs: Any,
|
602
|
+
) -> Tuple[Optional[Dict[str, Any]], Set[str]]:
|
589
603
|
if wms_layers is None:
|
590
604
|
wms_layers = []
|
591
605
|
if layers_name is None:
|
@@ -593,12 +607,12 @@ class Theme:
|
|
593
607
|
children = []
|
594
608
|
errors = set()
|
595
609
|
|
596
|
-
if re.search("[/?#]", group.name):
|
597
|
-
errors.add("The group has an unsupported name '{}'."
|
610
|
+
if re.search("[/?#]", group.name):
|
611
|
+
errors.add(f"The group has an unsupported name '{group.name}'.")
|
598
612
|
|
599
613
|
# escape loop
|
600
614
|
if depth > 30:
|
601
|
-
errors.add("Too many recursions with group '{}'"
|
615
|
+
errors.add(f"Too many recursions with group '{group.name}'")
|
602
616
|
return None, errors
|
603
617
|
|
604
618
|
ogc_servers = None
|
@@ -613,8 +627,8 @@ class Theme:
|
|
613
627
|
|
614
628
|
for tree_item in group.children:
|
615
629
|
if isinstance(tree_item, main.LayerGroup):
|
616
|
-
group_theme, gp_errors = self._group(
|
617
|
-
"{}/{
|
630
|
+
group_theme, gp_errors = await self._group(
|
631
|
+
f"{path}/{tree_item.name}",
|
618
632
|
tree_item,
|
619
633
|
layers,
|
620
634
|
depth=depth + 1,
|
@@ -635,14 +649,13 @@ class Theme:
|
|
635
649
|
if isinstance(tree_item, main.LayerWMS):
|
636
650
|
wms_layers.extend(tree_item.layer.split(","))
|
637
651
|
|
638
|
-
layer_theme, l_errors = self._layer(tree_item, mixed=mixed, time_=time_, dim=dim)
|
652
|
+
layer_theme, l_errors = await self._layer(tree_item, mixed=mixed, time_=time_, dim=dim)
|
639
653
|
errors |= l_errors
|
640
654
|
if layer_theme is not None:
|
641
655
|
if depth < min_levels:
|
642
656
|
errors.add(
|
643
|
-
"The Layer '{}' is under indented
|
644
|
-
|
645
|
-
)
|
657
|
+
f"The Layer '{path + '/' + tree_item.name}' is under indented "
|
658
|
+
f"({depth:d}/{min_levels:d})."
|
646
659
|
)
|
647
660
|
else:
|
648
661
|
children.append(layer_theme)
|
@@ -652,7 +665,7 @@ class Theme:
|
|
652
665
|
"id": group.id,
|
653
666
|
"name": group.name,
|
654
667
|
"children": children,
|
655
|
-
"metadata": self.
|
668
|
+
"metadata": self._get_metadata_list(group, errors),
|
656
669
|
"mixed": False,
|
657
670
|
}
|
658
671
|
if not mixed:
|
@@ -660,14 +673,16 @@ class Theme:
|
|
660
673
|
for name, nb in Counter(layers_name).items():
|
661
674
|
if nb > 1:
|
662
675
|
errors.add(
|
663
|
-
"The GeoMapFish layer name '{}', cannot be two times "
|
664
|
-
"in the same block (first level group)."
|
676
|
+
f"The GeoMapFish layer name '{name}', cannot be two times "
|
677
|
+
"in the same block (first level group)."
|
665
678
|
)
|
666
679
|
|
667
680
|
group_theme["mixed"] = mixed
|
668
681
|
if org_depth == 1:
|
669
682
|
if not mixed:
|
670
|
-
|
683
|
+
assert time_ is not None
|
684
|
+
assert dim is not None
|
685
|
+
group_theme["ogcServer"] = cast(List[Any], ogc_servers)[0]
|
671
686
|
if time_.has_time() and time_.layer is None:
|
672
687
|
group_theme["time"] = time_.to_dict()
|
673
688
|
|
@@ -676,13 +691,15 @@ class Theme:
|
|
676
691
|
return group_theme, errors
|
677
692
|
return None, errors
|
678
693
|
|
679
|
-
def _layers(self, interface):
|
694
|
+
def _layers(self, interface: str) -> List[str]:
|
680
695
|
query = self._create_layer_query(interface=interface)
|
681
696
|
return [name for (name,) in query.all()]
|
682
697
|
|
683
|
-
def _wms_layers(
|
698
|
+
async def _wms_layers(
|
699
|
+
self, ogc_server: main.OGCServer
|
700
|
+
) -> Tuple[Optional[Dict[str, Dict[str, Any]]], Set[str]]:
|
684
701
|
# retrieve layers metadata via GetCapabilities
|
685
|
-
wms, wms_errors =
|
702
|
+
wms, wms_errors = await self._wms_getcap(ogc_server)
|
686
703
|
if wms_errors:
|
687
704
|
return None, wms_errors
|
688
705
|
|
@@ -717,11 +734,10 @@ class Theme:
|
|
717
734
|
.all()
|
718
735
|
)
|
719
736
|
|
720
|
-
def _themes(
|
721
|
-
""
|
722
|
-
|
723
|
-
by ``role_id``.
|
724
|
-
"""
|
737
|
+
async def _themes(
|
738
|
+
self, interface: str = "desktop", filter_themes: bool = True, min_levels: int = 1
|
739
|
+
) -> Tuple[List[Dict[str, Any]], Set[str]]:
|
740
|
+
"""Return theme information for the role identified by ``role_id``."""
|
725
741
|
self._load_tree_items()
|
726
742
|
errors = set()
|
727
743
|
layers = self._layers(interface)
|
@@ -744,17 +760,22 @@ class Theme:
|
|
744
760
|
export_themes = []
|
745
761
|
for theme in themes.all():
|
746
762
|
if re.search("[/?#]", theme.name):
|
747
|
-
errors.add("The theme has an unsupported name '{}'."
|
763
|
+
errors.add(f"The theme has an unsupported name '{theme.name}'.")
|
748
764
|
continue
|
749
765
|
|
750
|
-
children, children_errors = self._get_children(theme, layers, min_levels)
|
766
|
+
children, children_errors = await self._get_children(theme, layers, min_levels)
|
751
767
|
errors |= children_errors
|
752
768
|
|
753
769
|
# Test if the theme is visible for the current user
|
754
770
|
if children:
|
755
|
-
|
756
|
-
get_url2("The Theme '{}'"
|
771
|
+
url = (
|
772
|
+
get_url2(f"The Theme '{theme.name}'", theme.icon, self.request, errors)
|
757
773
|
if theme.icon is not None and theme.icon
|
774
|
+
else None
|
775
|
+
)
|
776
|
+
icon = (
|
777
|
+
url.url()
|
778
|
+
if url is not None
|
758
779
|
else self.request.static_url("/etc/geomapfish/static/images/blank.png")
|
759
780
|
)
|
760
781
|
|
@@ -764,14 +785,14 @@ class Theme:
|
|
764
785
|
"icon": icon,
|
765
786
|
"children": children,
|
766
787
|
"functionalities": self._get_functionalities(theme),
|
767
|
-
"metadata": self.
|
788
|
+
"metadata": self._get_metadata_list(theme, errors),
|
768
789
|
}
|
769
790
|
export_themes.append(theme_theme)
|
770
791
|
|
771
792
|
return export_themes, errors
|
772
793
|
|
773
794
|
@staticmethod
|
774
|
-
def _get_functionalities(theme):
|
795
|
+
def _get_functionalities(theme: main.Theme) -> Dict[str, List[str]]:
|
775
796
|
result: Dict[str, List[str]] = {}
|
776
797
|
for functionality in theme.functionalities:
|
777
798
|
if functionality.name in result:
|
@@ -780,19 +801,21 @@ class Theme:
|
|
780
801
|
result[functionality.name] = [functionality.value]
|
781
802
|
return result
|
782
803
|
|
783
|
-
@view_config(route_name="invalidate", renderer="json")
|
784
|
-
def invalidate_cache(self)
|
804
|
+
@view_config(route_name="invalidate", renderer="json") # type: ignore
|
805
|
+
def invalidate_cache(self) -> Dict[str, bool]:
|
785
806
|
auth_view(self.request)
|
786
|
-
|
807
|
+
models.cache_invalidate_cb()
|
787
808
|
return {"success": True}
|
788
809
|
|
789
|
-
def _get_children(
|
810
|
+
async def _get_children(
|
811
|
+
self, theme: main.Theme, layers: List[str], min_levels: int
|
812
|
+
) -> Tuple[List[Dict[str, Any]], Set[str]]:
|
790
813
|
children = []
|
791
814
|
errors: Set[str] = set()
|
792
815
|
for item in theme.children:
|
793
816
|
if isinstance(item, main.LayerGroup):
|
794
|
-
group_theme, gp_errors = self._group(
|
795
|
-
"{}/{
|
817
|
+
group_theme, gp_errors = await self._group(
|
818
|
+
f"{theme.name}/{item.name}", item, layers, min_levels=min_levels
|
796
819
|
)
|
797
820
|
errors |= gp_errors
|
798
821
|
if group_theme is not None:
|
@@ -800,19 +823,18 @@ class Theme:
|
|
800
823
|
elif self._layer_included(item):
|
801
824
|
if min_levels > 0:
|
802
825
|
errors.add(
|
803
|
-
"The Layer '{}' cannot be directly in the theme '{}'
|
804
|
-
|
805
|
-
)
|
826
|
+
f"The Layer '{item.name}' cannot be directly in the theme '{theme.name}' "
|
827
|
+
f"(0/{min_levels:d})."
|
806
828
|
)
|
807
829
|
elif item.name in layers:
|
808
|
-
layer_theme, l_errors = self._layer(item, dim=DimensionInformation())
|
830
|
+
layer_theme, l_errors = await self._layer(item, dim=DimensionInformation())
|
809
831
|
errors |= l_errors
|
810
832
|
if layer_theme is not None:
|
811
833
|
children.append(layer_theme)
|
812
834
|
return children, errors
|
813
835
|
|
814
|
-
@CACHE_REGION.cache_on_arguments()
|
815
|
-
def _get_layers_enum(self):
|
836
|
+
@CACHE_REGION.cache_on_arguments() # type: ignore
|
837
|
+
def _get_layers_enum(self) -> Dict[str, Dict[str, str]]:
|
816
838
|
layers_enum = {}
|
817
839
|
if "enum" in self.settings.get("layers", {}):
|
818
840
|
for layer_name, layer in list(self.settings["layers"]["enum"].items()):
|
@@ -827,75 +849,90 @@ class Theme:
|
|
827
849
|
)
|
828
850
|
return layers_enum
|
829
851
|
|
830
|
-
def _get_role_ids(self):
|
852
|
+
def _get_role_ids(self) -> Optional[Set[int]]:
|
831
853
|
return None if self.request.user is None else {role.id for role in self.request.user.roles}
|
832
854
|
|
833
|
-
async def
|
855
|
+
async def _wfs_get_features_type(
|
856
|
+
self, wfs_url: Url, ogc_server: main.OGCServer, preload: bool = False, cache: bool = True
|
857
|
+
) -> Tuple[Optional[etree.Element], Set[str]]:
|
834
858
|
errors = set()
|
835
859
|
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
860
|
+
wfs_url.add_query(
|
861
|
+
{
|
862
|
+
"SERVICE": "WFS",
|
863
|
+
"VERSION": "1.0.0",
|
864
|
+
"REQUEST": "DescribeFeatureType",
|
865
|
+
"ROLE_IDS": "0",
|
866
|
+
"USER_ID": "0",
|
867
|
+
}
|
868
|
+
)
|
844
869
|
|
845
|
-
LOG.debug(
|
870
|
+
LOG.debug(
|
871
|
+
"Get the WFS DescribeFeatureType of '%s', preload: %s, cache: %s", ogc_server.name, preload, cache
|
872
|
+
)
|
846
873
|
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
874
|
+
headers = {}
|
875
|
+
|
876
|
+
# Add headers for Geoserver
|
877
|
+
if ogc_server.auth == main.OGCSERVER_AUTH_GEOSERVER:
|
878
|
+
headers["sec-username"] = "root"
|
879
|
+
headers["sec-roles"] = "root"
|
851
880
|
|
852
881
|
try:
|
853
|
-
|
854
|
-
|
882
|
+
content, _ = get_http_cached(self.http_options, wfs_url.url(), headers, cache)
|
883
|
+
except requests.exceptions.RequestException as exception:
|
884
|
+
error = (
|
885
|
+
f"Unable to get WFS DescribeFeatureType from the URL '{wfs_url.url()}' for "
|
886
|
+
f"OGC server {ogc_server.name}, "
|
887
|
+
+ (
|
888
|
+
f"return the error: {exception.response.status_code} {exception.response.reason}"
|
889
|
+
if exception.response is not None
|
890
|
+
else f"{exception}"
|
891
|
+
)
|
855
892
|
)
|
856
|
-
|
857
|
-
|
893
|
+
errors.add(error)
|
894
|
+
LOG.exception(error)
|
858
895
|
return None, errors
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
"
|
863
|
-
wfs_url, response.status_code, response.reason
|
864
|
-
)
|
896
|
+
except Exception:
|
897
|
+
error = (
|
898
|
+
f"Unable to get WFS DescribeFeatureType from the URL {wfs_url} for "
|
899
|
+
f"OGC server {ogc_server.name}"
|
865
900
|
)
|
901
|
+
errors.add(error)
|
902
|
+
LOG.exception(error)
|
866
903
|
return None, errors
|
867
904
|
|
868
905
|
if preload:
|
869
906
|
return None, errors
|
870
907
|
|
871
908
|
try:
|
872
|
-
return lxml.XML(
|
873
|
-
except Exception as e:
|
909
|
+
return lxml.XML(content), errors
|
910
|
+
except Exception as e:
|
874
911
|
errors.add(
|
875
|
-
"Error '{}' on reading DescribeFeatureType from URL {}:\n{}"
|
876
|
-
str(e), wfs_url, response.text
|
877
|
-
)
|
912
|
+
f"Error '{e!s}' on reading DescribeFeatureType from URL {wfs_url}:\n{content.decode()}"
|
878
913
|
)
|
879
914
|
return None, errors
|
880
915
|
|
881
|
-
def get_url_internal_wfs(
|
916
|
+
def get_url_internal_wfs(
|
917
|
+
self, ogc_server: main.OGCServer, errors: Set[str]
|
918
|
+
) -> Tuple[Optional[Url], Optional[Url], Optional[Url]]:
|
882
919
|
# required to do every time to validate the url.
|
883
920
|
if ogc_server.auth != main.OGCSERVER_AUTH_NOAUTH:
|
884
|
-
url =
|
885
|
-
|
921
|
+
url: Optional[Url] = Url(
|
922
|
+
self.request.route_url("mapserverproxy", _query={"ogcserver": ogc_server.name})
|
923
|
+
)
|
924
|
+
url_wfs: Optional[Url] = url
|
886
925
|
url_internal_wfs = get_url2(
|
887
|
-
"The OGC server (WFS) '{}'"
|
926
|
+
f"The OGC server (WFS) '{ogc_server.name}'",
|
888
927
|
ogc_server.url_wfs or ogc_server.url,
|
889
928
|
self.request,
|
890
929
|
errors=errors,
|
891
930
|
)
|
892
931
|
else:
|
893
|
-
url = get_url2(
|
894
|
-
"The OGC server '{}'".format(ogc_server.name), ogc_server.url, self.request, errors=errors
|
895
|
-
)
|
932
|
+
url = get_url2(f"The OGC server '{ogc_server.name}'", ogc_server.url, self.request, errors=errors)
|
896
933
|
url_wfs = (
|
897
934
|
get_url2(
|
898
|
-
"The OGC server (WFS) '{}'"
|
935
|
+
f"The OGC server (WFS) '{ogc_server.name}'",
|
899
936
|
ogc_server.url_wfs,
|
900
937
|
self.request,
|
901
938
|
errors=errors,
|
@@ -906,115 +943,178 @@ class Theme:
|
|
906
943
|
url_internal_wfs = url_wfs
|
907
944
|
return url_internal_wfs, url, url_wfs
|
908
945
|
|
909
|
-
async def
|
946
|
+
async def _preload(self, errors: Set[str]) -> None:
|
910
947
|
tasks = set()
|
911
948
|
for ogc_server in models.DBSession.query(main.OGCServer).all():
|
912
|
-
|
913
|
-
|
914
|
-
|
949
|
+
# Don't load unused OGC servers, required for landing page, because the related OGC server
|
950
|
+
# will be on error in those functions.
|
951
|
+
nb_layers = (
|
952
|
+
models.DBSession.query(sqlalchemy.func.count(main.LayerWMS.id))
|
953
|
+
.filter(main.LayerWMS.ogc_server_id == ogc_server.id)
|
954
|
+
.one()
|
955
|
+
)
|
956
|
+
LOG.debug("%i layers for OGC server '%s'", nb_layers[0], ogc_server.name)
|
957
|
+
if nb_layers[0] > 0:
|
958
|
+
LOG.debug("Preload OGC server '%s'", ogc_server.name)
|
959
|
+
url_internal_wfs, _, _ = self.get_url_internal_wfs(ogc_server, errors)
|
960
|
+
if url_internal_wfs is not None:
|
961
|
+
tasks.add(self.preload_ogc_server(ogc_server, url_internal_wfs))
|
915
962
|
|
916
963
|
await asyncio.gather(*tasks)
|
917
964
|
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
965
|
+
async def preload_ogc_server(
|
966
|
+
self, ogc_server: main.OGCServer, url_internal_wfs: Url, cache: bool = True
|
967
|
+
) -> None:
|
968
|
+
if ogc_server.wfs_support:
|
969
|
+
await self._get_features_attributes(url_internal_wfs, ogc_server, cache=cache)
|
970
|
+
await self._wms_getcap(ogc_server, False, cache=cache)
|
971
|
+
|
972
|
+
async def _get_features_attributes(
|
973
|
+
self, url_internal_wfs: Url, ogc_server: main.OGCServer, cache: bool = True
|
974
|
+
) -> Tuple[Optional[Dict[str, Dict[Any, Dict[str, Any]]]], Optional[str], Set[str]]:
|
975
|
+
@CACHE_OGC_SERVER_REGION.cache_on_arguments() # type: ignore
|
976
|
+
def _get_features_attributes_cache(
|
977
|
+
url_internal_wfs: Url, ogc_server_name: str
|
978
|
+
) -> Tuple[Optional[Dict[str, Dict[Any, Dict[str, Any]]]], Optional[str], Set[str]]:
|
979
|
+
del url_internal_wfs # Just for cache
|
980
|
+
all_errors: Set[str] = set()
|
981
|
+
if errors:
|
982
|
+
all_errors |= errors
|
983
|
+
return None, None, all_errors
|
984
|
+
assert feature_type is not None
|
985
|
+
namespace: str = feature_type.attrib.get("targetNamespace")
|
986
|
+
types: Dict[Any, Dict[str, Any]] = {}
|
987
|
+
elements = {}
|
988
|
+
for child in feature_type.getchildren():
|
989
|
+
if child.tag == "{http://www.w3.org/2001/XMLSchema}element":
|
990
|
+
name = child.attrib["name"]
|
991
|
+
type_namespace, type_ = child.attrib["type"].split(":")
|
992
|
+
if type_namespace not in child.nsmap:
|
993
|
+
LOG.info(
|
994
|
+
"The namespace '%s' of the type '%s' is not found in the "
|
995
|
+
"available namespaces: %s (OGC server: %s)",
|
996
|
+
type_namespace,
|
997
|
+
name,
|
998
|
+
", ".join([str(k) for k in child.nsmap.keys()]),
|
999
|
+
ogc_server_name,
|
1000
|
+
)
|
1001
|
+
elif child.nsmap[type_namespace] != namespace:
|
1002
|
+
LOG.info(
|
1003
|
+
"The namespace '%s' of the type '%s' should be '%s' (OGC server: %s).",
|
1004
|
+
child.nsmap[type_namespace],
|
1005
|
+
name,
|
1006
|
+
namespace,
|
1007
|
+
ogc_server_name,
|
1008
|
+
)
|
1009
|
+
elements[name] = type_
|
1010
|
+
|
1011
|
+
if child.tag == "{http://www.w3.org/2001/XMLSchema}complexType":
|
1012
|
+
sequence = child.find(".//{http://www.w3.org/2001/XMLSchema}sequence")
|
1013
|
+
attrib = {}
|
1014
|
+
for children in sequence.getchildren():
|
1015
|
+
type_namespace = None
|
1016
|
+
type_ = children.attrib["type"]
|
1017
|
+
if len(type_.split(":")) == 2:
|
1018
|
+
type_namespace, type_ = type_.split(":")
|
1019
|
+
name = children.attrib["name"]
|
1020
|
+
attrib[name] = {"type": type_}
|
1021
|
+
if type_namespace in children.nsmap:
|
1022
|
+
type_namespace = children.nsmap[type_namespace]
|
1023
|
+
attrib[name]["namespace"] = type_namespace
|
1024
|
+
else:
|
1025
|
+
LOG.info(
|
1026
|
+
"The namespace '%s' of the type '%s' is not found in the "
|
1027
|
+
"available namespaces: %s (OGC server: %s)",
|
1028
|
+
type_namespace,
|
1029
|
+
name,
|
1030
|
+
", ".join([str(k) for k in child.nsmap.keys()]),
|
1031
|
+
ogc_server_name,
|
1032
|
+
)
|
1033
|
+
for key, value in children.attrib.items():
|
1034
|
+
if key not in ("name", "type", "namespace"):
|
1035
|
+
attrib[name][key] = value
|
1036
|
+
types[child.attrib["name"]] = attrib
|
1037
|
+
attributes: Dict[str, Dict[Any, Dict[str, Any]]] = {}
|
1038
|
+
for name, type_ in elements.items():
|
1039
|
+
if type_ in types:
|
1040
|
+
attributes[name] = types[type_]
|
1041
|
+
elif (type_ == "Character") and (name + "Type") in types:
|
1042
|
+
LOG.debug(
|
1043
|
+
'Due to MapServer weird behavior when using METADATA "gml_types" "auto"'
|
1044
|
+
"the type 'ms:Character' is returned as type '%sType' for feature '%s'.",
|
937
1045
|
name,
|
938
|
-
", ".join(child.nsmap.keys()),
|
939
|
-
)
|
940
|
-
if child.nsmap[type_namespace] != namespace:
|
941
|
-
LOG.info(
|
942
|
-
"The namespace '%s' of the thye '%s' should be '%s'.",
|
943
|
-
child.nsmap[type_namespace],
|
944
1046
|
name,
|
945
|
-
namespace,
|
946
1047
|
)
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
if key not in ("name", "type", "namespace"):
|
962
|
-
attrib[name][key] = value
|
963
|
-
types[child.attrib["name"]] = attrib
|
964
|
-
attributes = {}
|
965
|
-
for name, type_ in elements.items():
|
966
|
-
if type_ in types:
|
967
|
-
attributes[name] = types[type_]
|
968
|
-
elif (type_ == "Character") and (name + "Type") in types:
|
969
|
-
LOG.debug(
|
970
|
-
"Due mapserver strange result the type 'ms:Character' is fallbacked to type '%sType'"
|
971
|
-
" for feature '%s', This is a strange comportement of mapserver when we use the "
|
972
|
-
'METADATA "gml_types" "auto"',
|
973
|
-
name,
|
974
|
-
name,
|
975
|
-
)
|
976
|
-
attributes[name] = types[name + "Type"]
|
977
|
-
else:
|
978
|
-
LOG.warning(
|
979
|
-
"The provided type '%s' does not exist, available types are %s.",
|
980
|
-
type_,
|
981
|
-
", ".join(types.keys()),
|
982
|
-
)
|
1048
|
+
attributes[name] = types[name + "Type"]
|
1049
|
+
else:
|
1050
|
+
LOG.warning(
|
1051
|
+
"The provided type '%s' does not exist, available types are %s.",
|
1052
|
+
type_,
|
1053
|
+
", ".join(types.keys()),
|
1054
|
+
)
|
1055
|
+
|
1056
|
+
return attributes, namespace, all_errors
|
1057
|
+
|
1058
|
+
if cache:
|
1059
|
+
result = _get_features_attributes_cache.get(url_internal_wfs, ogc_server.name)
|
1060
|
+
if result != dogpile.cache.api.NO_VALUE:
|
1061
|
+
return result # type: ignore
|
983
1062
|
|
984
|
-
|
1063
|
+
feature_type, errors = await self._wfs_get_features_type(url_internal_wfs, ogc_server, False, cache)
|
985
1064
|
|
986
|
-
|
987
|
-
|
1065
|
+
return _get_features_attributes_cache.refresh(url_internal_wfs, ogc_server.name) # type: ignore
|
1066
|
+
|
1067
|
+
@view_config(route_name="themes", renderer="json") # type: ignore
|
1068
|
+
def themes(self) -> Dict[str, Union[Dict[str, Dict[str, Any]], List[str]]]:
|
988
1069
|
interface = self.request.params.get("interface", "desktop")
|
989
1070
|
sets = self.request.params.get("set", "all")
|
990
1071
|
min_levels = int(self.request.params.get("min_levels", 1))
|
991
1072
|
group = self.request.params.get("group")
|
992
1073
|
background_layers_group = self.request.params.get("background")
|
993
1074
|
|
994
|
-
set_common_headers(self.request, "themes",
|
1075
|
+
set_common_headers(self.request, "themes", Cache.PRIVATE)
|
995
1076
|
|
996
|
-
def get_theme():
|
1077
|
+
async def get_theme() -> Dict[str, Union[Dict[str, Any], List[str]]]:
|
997
1078
|
export_themes = sets in ("all", "themes")
|
998
1079
|
export_group = group is not None and sets in ("all", "group")
|
999
1080
|
export_background = background_layers_group is not None and sets in ("all", "background")
|
1000
1081
|
|
1001
|
-
result: Dict[str, Union[Dict[str,
|
1082
|
+
result: Dict[str, Union[Dict[str, Any], List[Any]]] = {}
|
1002
1083
|
all_errors: Set[str] = set()
|
1003
1084
|
LOG.debug("Start preload")
|
1004
1085
|
start_time = time.time()
|
1005
|
-
|
1086
|
+
await self._preload(all_errors)
|
1006
1087
|
LOG.debug("End preload")
|
1007
1088
|
# Don't log if it looks to be already preloaded.
|
1008
1089
|
if (time.time() - start_time) > 1:
|
1009
1090
|
LOG.info("Do preload in %.3fs.", time.time() - start_time)
|
1091
|
+
LOG.debug("Run garbage collection: %s", ", ".join([str(gc.collect(n)) for n in range(3)]))
|
1010
1092
|
result["ogcServers"] = {}
|
1011
1093
|
for ogc_server in models.DBSession.query(main.OGCServer).all():
|
1094
|
+
nb_layers = (
|
1095
|
+
models.DBSession.query(sqlalchemy.func.count(main.LayerWMS.id))
|
1096
|
+
.filter(main.LayerWMS.ogc_server_id == ogc_server.id)
|
1097
|
+
.one()
|
1098
|
+
)
|
1099
|
+
if nb_layers[0] == 0:
|
1100
|
+
# QGIS Server landing page requires an OGC server that can't be used here.
|
1101
|
+
continue
|
1102
|
+
|
1103
|
+
LOG.debug("Process OGC server '%s'", ogc_server.name)
|
1104
|
+
|
1012
1105
|
url_internal_wfs, url, url_wfs = self.get_url_internal_wfs(ogc_server, all_errors)
|
1013
1106
|
|
1014
1107
|
attributes = None
|
1015
1108
|
namespace = None
|
1016
|
-
if ogc_server.wfs_support:
|
1017
|
-
|
1109
|
+
if ogc_server.wfs_support and not url_internal_wfs:
|
1110
|
+
all_errors.add(
|
1111
|
+
f"The OGC server '{ogc_server.name}' is configured to support WFS "
|
1112
|
+
"but no internal WFS URL is found."
|
1113
|
+
)
|
1114
|
+
if ogc_server.wfs_support and url_internal_wfs:
|
1115
|
+
attributes, namespace, errors = await self._get_features_attributes(
|
1116
|
+
url_internal_wfs, ogc_server
|
1117
|
+
)
|
1018
1118
|
# Create a local copy (don't modify the cache)
|
1019
1119
|
if attributes is not None:
|
1020
1120
|
attributes = dict(attributes)
|
@@ -1036,8 +1136,8 @@ class Theme:
|
|
1036
1136
|
del attributes[name]
|
1037
1137
|
|
1038
1138
|
result["ogcServers"][ogc_server.name] = {
|
1039
|
-
"url": url,
|
1040
|
-
"urlWfs": url_wfs,
|
1139
|
+
"url": url.url() if url else None,
|
1140
|
+
"urlWfs": url_wfs.url() if url_wfs else None,
|
1041
1141
|
"type": ogc_server.type,
|
1042
1142
|
"credential": ogc_server.auth != main.OGCSERVER_AUTH_NOAUTH,
|
1043
1143
|
"imageType": ogc_server.image_type,
|
@@ -1047,19 +1147,19 @@ class Theme:
|
|
1047
1147
|
"attributes": attributes,
|
1048
1148
|
}
|
1049
1149
|
if export_themes:
|
1050
|
-
themes, errors = self._themes(interface, True, min_levels)
|
1150
|
+
themes, errors = await self._themes(interface, True, min_levels)
|
1051
1151
|
|
1052
1152
|
result["themes"] = themes
|
1053
1153
|
all_errors |= errors
|
1054
1154
|
|
1055
1155
|
if export_group:
|
1056
|
-
exported_group, errors = self._get_group(group, interface)
|
1156
|
+
exported_group, errors = await self._get_group(group, interface)
|
1057
1157
|
if exported_group is not None:
|
1058
1158
|
result["group"] = exported_group
|
1059
1159
|
all_errors |= errors
|
1060
1160
|
|
1061
1161
|
if export_background:
|
1062
|
-
exported_group, errors = self._get_group(background_layers_group, interface)
|
1162
|
+
exported_group, errors = await self._get_group(background_layers_group, interface)
|
1063
1163
|
result["background_layers"] = exported_group["children"] if exported_group is not None else []
|
1064
1164
|
all_errors |= errors
|
1065
1165
|
|
@@ -1068,38 +1168,78 @@ class Theme:
|
|
1068
1168
|
LOG.info("Theme errors:\n%s", "\n".join(all_errors))
|
1069
1169
|
return result
|
1070
1170
|
|
1071
|
-
@CACHE_REGION.cache_on_arguments()
|
1072
|
-
def get_theme_anonymous(
|
1171
|
+
@CACHE_REGION.cache_on_arguments() # type: ignore
|
1172
|
+
def get_theme_anonymous(
|
1173
|
+
intranet: bool,
|
1174
|
+
interface: str,
|
1175
|
+
sets: str,
|
1176
|
+
min_levels: str,
|
1177
|
+
group: str,
|
1178
|
+
background_layers_group: str,
|
1179
|
+
host: str,
|
1180
|
+
) -> Dict[str, Union[Dict[str, Dict[str, Any]], List[str]]]:
|
1073
1181
|
# Only for cache key
|
1074
1182
|
del intranet, interface, sets, min_levels, group, background_layers_group, host
|
1075
|
-
return get_theme()
|
1183
|
+
return asyncio.run(get_theme())
|
1076
1184
|
|
1077
1185
|
if self.request.user is None:
|
1078
|
-
return
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
|
1083
|
-
|
1084
|
-
|
1085
|
-
|
1186
|
+
return cast(
|
1187
|
+
Dict[str, Union[Dict[str, Dict[str, Any]], List[str]]],
|
1188
|
+
get_theme_anonymous(
|
1189
|
+
is_intranet(self.request),
|
1190
|
+
interface,
|
1191
|
+
sets,
|
1192
|
+
min_levels,
|
1193
|
+
group,
|
1194
|
+
background_layers_group,
|
1195
|
+
self.request.headers.get("Host"),
|
1196
|
+
),
|
1086
1197
|
)
|
1087
|
-
return get_theme()
|
1198
|
+
return asyncio.run(get_theme())
|
1088
1199
|
|
1089
|
-
def _get_group(
|
1200
|
+
async def _get_group(
|
1201
|
+
self, group: main.LayerGroup, interface: main.Interface
|
1202
|
+
) -> Tuple[Optional[Dict[str, Any]], Set[str]]:
|
1090
1203
|
layers = self._layers(interface)
|
1091
1204
|
try:
|
1092
1205
|
group_db = models.DBSession.query(main.LayerGroup).filter(main.LayerGroup.name == group).one()
|
1093
|
-
return self._group(group_db.name, group_db, layers, depth=2, dim=DimensionInformation())
|
1094
|
-
except NoResultFound:
|
1206
|
+
return await self._group(group_db.name, group_db, layers, depth=2, dim=DimensionInformation())
|
1207
|
+
except NoResultFound:
|
1095
1208
|
return (
|
1096
1209
|
None,
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
", ".join([i[0] for i in models.DBSession.query(main.LayerGroup.name).all()]),
|
1102
|
-
)
|
1103
|
-
]
|
1104
|
-
),
|
1210
|
+
{
|
1211
|
+
f"Unable to find the Group named: {group}, Available Groups: "
|
1212
|
+
f"{', '.join([i[0] for i in models.DBSession.query(main.LayerGroup.name).all()])}"
|
1213
|
+
},
|
1105
1214
|
)
|
1215
|
+
|
1216
|
+
@view_config(route_name="ogc_server_clear_cache", renderer="json") # type: ignore
|
1217
|
+
def ogc_server_clear_cache_view(self) -> Dict[str, Any]:
|
1218
|
+
self._ogc_server_clear_cache(
|
1219
|
+
models.DBSession.query(main.OGCServer).filter_by(id=self.request.matchdict.get("id")).one()
|
1220
|
+
)
|
1221
|
+
came_from = self.request.params.get("came_from")
|
1222
|
+
if came_from:
|
1223
|
+
raise pyramid.httpexceptions.HTTPFound(location=came_from)
|
1224
|
+
return {"success": True}
|
1225
|
+
|
1226
|
+
def _ogc_server_clear_cache(self, ogc_server: main.OGCServer) -> None:
|
1227
|
+
errors: Set[str] = set()
|
1228
|
+
url_internal_wfs, _, _ = self.get_url_internal_wfs(ogc_server, errors)
|
1229
|
+
if errors:
|
1230
|
+
LOG.error(
|
1231
|
+
"Error while getting the URL of the OGC Server %s:\n%s", ogc_server.id, "\n".join(errors)
|
1232
|
+
)
|
1233
|
+
return
|
1234
|
+
if url_internal_wfs is None:
|
1235
|
+
return
|
1236
|
+
|
1237
|
+
asyncio.run(self._async_cache_invalidate_ogc_server_cb(ogc_server, url_internal_wfs))
|
1238
|
+
|
1239
|
+
async def _async_cache_invalidate_ogc_server_cb(
|
1240
|
+
self, ogc_server: main.OGCServer, url_internal_wfs: Url
|
1241
|
+
) -> None:
|
1242
|
+
# Fill the cache
|
1243
|
+
await self.preload_ogc_server(ogc_server, url_internal_wfs, False)
|
1244
|
+
|
1245
|
+
cache_invalidate_cb()
|