c2cgeoportal-geoportal 2.5.0.100__py2.py3-none-any.whl → 2.7.1.83__py2.py3-none-any.whl

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