c2cgeoportal-geoportal 2.6.0__py2.py3-none-any.whl → 2.8.1.180__py2.py3-none-any.whl

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