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