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