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