c2cgeoportal-geoportal 2.3.5.79__py3-none-any.whl → 2.9rc1__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 (197) hide show
  1. c2cgeoportal_geoportal/__init__.py +960 -0
  2. c2cgeoportal_geoportal/lib/__init__.py +256 -0
  3. c2cgeoportal_geoportal/lib/authentication.py +250 -0
  4. c2cgeoportal_geoportal/lib/bashcolor.py +46 -0
  5. c2cgeoportal_geoportal/lib/cacheversion.py +75 -0
  6. c2cgeoportal_geoportal/lib/caching.py +176 -0
  7. c2cgeoportal_geoportal/lib/check_collector.py +80 -0
  8. c2cgeoportal_geoportal/lib/checker.py +295 -0
  9. c2cgeoportal_geoportal/lib/common_headers.py +170 -0
  10. c2cgeoportal_geoportal/lib/dbreflection.py +266 -0
  11. c2cgeoportal_geoportal/lib/filter_capabilities.py +360 -0
  12. c2cgeoportal_geoportal/lib/fulltextsearch.py +50 -0
  13. c2cgeoportal_geoportal/lib/functionality.py +166 -0
  14. c2cgeoportal_geoportal/lib/headers.py +62 -0
  15. c2cgeoportal_geoportal/lib/i18n.py +38 -0
  16. c2cgeoportal_geoportal/lib/layers.py +132 -0
  17. c2cgeoportal_geoportal/lib/lingva_extractor.py +937 -0
  18. c2cgeoportal_geoportal/lib/loader.py +57 -0
  19. c2cgeoportal_geoportal/lib/metrics.py +117 -0
  20. c2cgeoportal_geoportal/lib/oauth2.py +1186 -0
  21. c2cgeoportal_geoportal/lib/oidc.py +302 -0
  22. c2cgeoportal_geoportal/lib/wmstparsing.py +353 -0
  23. c2cgeoportal_geoportal/lib/xsd.py +166 -0
  24. c2cgeoportal_geoportal/py.typed +0 -0
  25. c2cgeoportal_geoportal/resources.py +49 -0
  26. c2cgeoportal_geoportal/scaffolds/advance_create/ci/config.yaml +26 -0
  27. c2cgeoportal_geoportal/scaffolds/advance_create/cookiecutter.json +18 -0
  28. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/.dockerignore +6 -0
  29. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/.eslintrc.yaml +19 -0
  30. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/.prospector.yaml +30 -0
  31. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Dockerfile +75 -0
  32. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Makefile +6 -0
  33. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/alembic.ini +58 -0
  34. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/alembic.yaml +19 -0
  35. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/development.ini +121 -0
  36. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/gunicorn.conf.py +139 -0
  37. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/language_mapping +3 -0
  38. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/lingva-client.cfg +5 -0
  39. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/lingva-server.cfg +6 -0
  40. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/production.ini +38 -0
  41. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/requirements.txt +2 -0
  42. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/setup.py +25 -0
  43. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.api.js +41 -0
  44. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.apps.js +64 -0
  45. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.commons.js +11 -0
  46. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.config.js +22 -0
  47. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/__init__.py +42 -0
  48. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/authentication.py +10 -0
  49. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/dev.py +14 -0
  50. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/models.py +8 -0
  51. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/multi_organization.py +7 -0
  52. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/resources.py +11 -0
  53. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static-ngeo/api/index.js +12 -0
  54. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static-ngeo/js/{{cookiecutter.package}}module.js +25 -0
  55. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/subscribers.py +39 -0
  56. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/views/__init__.py +0 -0
  57. c2cgeoportal_geoportal/scaffolds/advance_update/cookiecutter.json +18 -0
  58. c2cgeoportal_geoportal/scaffolds/advance_update/{{cookiecutter.project}}/geoportal/CONST_Makefile +121 -0
  59. c2cgeoportal_geoportal/scaffolds/create/cookiecutter.json +18 -0
  60. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.dockerignore +14 -0
  61. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.editorconfig +17 -0
  62. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/main.yaml +73 -0
  63. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/rebuild.yaml +50 -0
  64. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/update_l10n.yaml +66 -0
  65. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.gitignore +16 -0
  66. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.pre-commit-config.yaml +35 -0
  67. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.prettierignore +1 -0
  68. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.prettierrc.yaml +2 -0
  69. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Dockerfile +75 -0
  70. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Makefile +70 -0
  71. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/README.rst +29 -0
  72. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/build +179 -0
  73. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/config.yaml +22 -0
  74. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/docker-compose-check +25 -0
  75. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/requirements.txt +2 -0
  76. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-db.yaml +24 -0
  77. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-lib.yaml +511 -0
  78. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-qgis.yaml +21 -0
  79. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose.override.sample.yaml +59 -0
  80. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose.yaml +121 -0
  81. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.default +102 -0
  82. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.project +69 -0
  83. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/vars.yaml +430 -0
  84. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/locale/en/LC_MESSAGES/{{cookiecutter.package}}_geoportal-client.po +6 -0
  85. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/css/desktop.css +0 -0
  86. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/css/iframe_api.css +0 -0
  87. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/css/mobile.css +0 -0
  88. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/banner_left.png +0 -0
  89. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/banner_right.png +0 -0
  90. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/blank.png +0 -0
  91. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/markers/marker-blue.png +0 -0
  92. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/markers/marker-gold.png +0 -0
  93. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/markers/marker-green.png +0 -0
  94. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/markers/marker.png +0 -0
  95. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/robot.txt.tmpl +3 -0
  96. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/data/Readme.txt +69 -0
  97. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/data/TM_EUROPE_BORDERS-0.3.sql +70 -0
  98. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/demo.map.tmpl +224 -0
  99. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Arial.ttf +0 -0
  100. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Arialbd.ttf +0 -0
  101. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Arialbi.ttf +0 -0
  102. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Ariali.ttf +0 -0
  103. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/NotoSans-Bold.ttf +0 -0
  104. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/NotoSans-BoldItalic.ttf +0 -0
  105. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/NotoSans-Italic.ttf +0 -0
  106. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/NotoSans-Regular.ttf +0 -0
  107. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Verdana.ttf +0 -0
  108. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Verdanab.ttf +0 -0
  109. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Verdanai.ttf +0 -0
  110. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Verdanaz.ttf +0 -0
  111. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts.conf +12 -0
  112. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/mapserver.conf +15 -0
  113. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/mapserver.map.tmpl +87 -0
  114. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/tinyows.xml.tmpl +36 -0
  115. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A3_Landscape.jrxml +207 -0
  116. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A3_Portrait.jrxml +185 -0
  117. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A4_Landscape.jrxml +200 -0
  118. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A4_Portrait.jrxml +170 -0
  119. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/config.yaml.tmpl +175 -0
  120. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/legend.jrxml +109 -0
  121. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/localisation.properties +4 -0
  122. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/localisation_fr.properties +4 -0
  123. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/logo.png +0 -0
  124. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/north.svg +93 -0
  125. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/results.jrxml +25 -0
  126. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/project.yaml +18 -0
  127. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/pyproject.toml +7 -0
  128. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/qgisserver/pg_service.conf.tmpl +15 -0
  129. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/run_alembic.sh +11 -0
  130. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/db-backup +126 -0
  131. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/db-restore +132 -0
  132. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/setup.cfg +7 -0
  133. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/spell-ignore-words.txt +5 -0
  134. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tests/__init__.py +0 -0
  135. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tests/test_app.py +43 -0
  136. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tilegeneration/config.yaml.tmpl +195 -0
  137. c2cgeoportal_geoportal/scaffolds/update/cookiecutter.json +18 -0
  138. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/.upgrade.yaml +67 -0
  139. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/CONST_CHANGELOG.txt +295 -0
  140. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/CONST_create_template/tests/test_testapp.py +48 -0
  141. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_config-schema.yaml +922 -0
  142. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_vars.yaml +1503 -0
  143. c2cgeoportal_geoportal/scripts/__init__.py +64 -0
  144. c2cgeoportal_geoportal/scripts/c2cupgrade.py +879 -0
  145. c2cgeoportal_geoportal/scripts/create_demo_theme.py +80 -0
  146. c2cgeoportal_geoportal/scripts/manage_users.py +140 -0
  147. c2cgeoportal_geoportal/scripts/pcreate.py +314 -0
  148. c2cgeoportal_geoportal/scripts/theme2fts.py +347 -0
  149. c2cgeoportal_geoportal/scripts/urllogin.py +81 -0
  150. c2cgeoportal_geoportal/templates/login.html +90 -0
  151. c2cgeoportal_geoportal/templates/notlogin.html +62 -0
  152. c2cgeoportal_geoportal/templates/testi18n.html +12 -0
  153. c2cgeoportal_geoportal/views/__init__.py +59 -0
  154. c2cgeoportal_geoportal/views/dev.py +57 -0
  155. c2cgeoportal_geoportal/views/dynamic.py +208 -0
  156. c2cgeoportal_geoportal/views/entry.py +174 -0
  157. c2cgeoportal_geoportal/views/fulltextsearch.py +189 -0
  158. c2cgeoportal_geoportal/views/geometry_processing.py +75 -0
  159. c2cgeoportal_geoportal/views/i18n.py +129 -0
  160. c2cgeoportal_geoportal/views/layers.py +713 -0
  161. c2cgeoportal_geoportal/views/login.py +679 -0
  162. c2cgeoportal_geoportal/views/mapserverproxy.py +191 -0
  163. c2cgeoportal_geoportal/views/memory.py +90 -0
  164. c2cgeoportal_geoportal/views/ogcproxy.py +120 -0
  165. c2cgeoportal_geoportal/views/pdfreport.py +245 -0
  166. c2cgeoportal_geoportal/views/printproxy.py +143 -0
  167. c2cgeoportal_geoportal/views/profile.py +127 -0
  168. c2cgeoportal_geoportal/views/proxy.py +259 -0
  169. c2cgeoportal_geoportal/views/raster.py +193 -0
  170. c2cgeoportal_geoportal/views/resourceproxy.py +73 -0
  171. c2cgeoportal_geoportal/views/shortener.py +152 -0
  172. c2cgeoportal_geoportal/views/theme.py +1322 -0
  173. c2cgeoportal_geoportal/views/tinyowsproxy.py +189 -0
  174. c2cgeoportal_geoportal/views/vector_tiles.py +83 -0
  175. {c2cgeoportal_geoportal-2.3.5.79.dist-info → c2cgeoportal_geoportal-2.9rc1.dist-info}/METADATA +21 -24
  176. c2cgeoportal_geoportal-2.9rc1.dist-info/RECORD +192 -0
  177. {c2cgeoportal_geoportal-2.3.5.79.dist-info → c2cgeoportal_geoportal-2.9rc1.dist-info}/WHEEL +1 -1
  178. c2cgeoportal_geoportal-2.9rc1.dist-info/entry_points.txt +28 -0
  179. c2cgeoportal_geoportal-2.9rc1.dist-info/top_level.txt +2 -0
  180. tests/__init__.py +100 -0
  181. tests/test_cachebuster.py +71 -0
  182. tests/test_caching.py +275 -0
  183. tests/test_checker.py +85 -0
  184. tests/test_decimaljson.py +47 -0
  185. tests/test_headerstween.py +64 -0
  186. tests/test_i18n.py +31 -0
  187. tests/test_init.py +193 -0
  188. tests/test_locale_negociator.py +69 -0
  189. tests/test_mapserverproxy_route_predicate.py +64 -0
  190. tests/test_raster.py +267 -0
  191. tests/test_wmstparsing.py +238 -0
  192. tests/xmlstr.py +103 -0
  193. c2cgeoportal_geoportal-2.3.5.79.dist-info/DESCRIPTION.rst +0 -8
  194. c2cgeoportal_geoportal-2.3.5.79.dist-info/RECORD +0 -7
  195. c2cgeoportal_geoportal-2.3.5.79.dist-info/entry_points.txt +0 -22
  196. c2cgeoportal_geoportal-2.3.5.79.dist-info/metadata.json +0 -1
  197. c2cgeoportal_geoportal-2.3.5.79.dist-info/top_level.txt +0 -1
@@ -0,0 +1,713 @@
1
+ # Copyright (c) 2012-2024, Camptocamp SA
2
+ # All rights reserved.
3
+
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+
7
+ # 1. Redistributions of source code must retain the above copyright notice, this
8
+ # list of conditions and the following disclaimer.
9
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ # this list of conditions and the following disclaimer in the documentation
11
+ # and/or other materials provided with the distribution.
12
+
13
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
+ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
+
24
+ # The views and conclusions contained in the software and documentation are those
25
+ # of the authors and should not be interpreted as representing official policies,
26
+ # either expressed or implied, of the FreeBSD Project.
27
+
28
+ import json
29
+ import logging
30
+ import os
31
+ from collections.abc import Generator
32
+ from datetime import datetime
33
+ from typing import TYPE_CHECKING, Any, TypedDict, cast
34
+
35
+ import geoalchemy2.elements
36
+ import geojson.geometry
37
+ import pyramid.request
38
+ import pyramid.response
39
+ import shapely.geometry
40
+ import sqlalchemy.ext.declarative
41
+ import sqlalchemy.orm
42
+ import sqlalchemy.orm.query
43
+ from geoalchemy2 import Geometry
44
+ from geoalchemy2.shape import from_shape, to_shape
45
+ from geojson.feature import Feature, FeatureCollection
46
+ from papyrus.protocol import Protocol, create_filter
47
+ from papyrus.xsd import XSDGenerator
48
+ from pyramid.httpexceptions import (
49
+ HTTPBadRequest,
50
+ HTTPException,
51
+ HTTPForbidden,
52
+ HTTPInternalServerError,
53
+ HTTPNotFound,
54
+ )
55
+ from pyramid.view import view_config
56
+ from shapely import unary_union
57
+ from shapely.errors import TopologicalError
58
+ from sqlalchemy import Enum, Numeric, String, Text, Unicode, UnicodeText, exc, func
59
+ from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound # type: ignore[attr-defined]
60
+ from sqlalchemy.orm.properties import ColumnProperty
61
+ from sqlalchemy.orm.util import class_mapper
62
+ from sqlalchemy.sql import and_, or_
63
+
64
+ from c2cgeoportal_commons import models
65
+ from c2cgeoportal_geoportal.lib import get_roles_id
66
+ from c2cgeoportal_geoportal.lib.caching import get_region
67
+ from c2cgeoportal_geoportal.lib.common_headers import Cache, set_common_headers
68
+ from c2cgeoportal_geoportal.lib.dbreflection import _AssociationProxy, get_class, get_table
69
+
70
+ if TYPE_CHECKING:
71
+ from c2cgeoportal_commons.models import main # pylint: disable=ungrouped-imports.useless-suppression
72
+
73
+
74
+ _LOG = logging.getLogger(__name__)
75
+ _CACHE_REGION = get_region("std")
76
+
77
+
78
+ class _BaseCallback:
79
+ def __init__(self, layer: "main.Layer"):
80
+ self.layer = layer
81
+
82
+ def update(self, request: pyramid.request.Request, obj: Any) -> None:
83
+ last_update_date = Layers.get_metadata(self.layer, "lastUpdateDateColumn")
84
+ if last_update_date is not None:
85
+ setattr(obj, last_update_date, datetime.now())
86
+
87
+ last_update_user = Layers.get_metadata(self.layer, "lastUpdateUserColumn")
88
+ if last_update_user is not None:
89
+ setattr(obj, last_update_user, request.user.id)
90
+
91
+ def _get_geometry_check_base_query(
92
+ self, request: pyramid.request.Request
93
+ ) -> sqlalchemy.orm.query.RowReturningQuery[tuple[int]]:
94
+ from c2cgeoportal_commons.models.main import ( # pylint: disable=import-outside-toplevel
95
+ Layer,
96
+ RestrictionArea,
97
+ Role,
98
+ )
99
+
100
+ assert models.DBSession is not None
101
+ allowed = models.DBSession.query(func.count(RestrictionArea.id)) # pylint: disable=not-callable
102
+ allowed = allowed.join(RestrictionArea.roles)
103
+ allowed = allowed.join(RestrictionArea.layers)
104
+ allowed = allowed.filter(RestrictionArea.readwrite.is_(True))
105
+ allowed = allowed.filter(Role.id.in_(get_roles_id(request)))
106
+ allowed = allowed.filter(Layer.id == self.layer.id)
107
+ return allowed
108
+
109
+
110
+ class _InsertCallback(_BaseCallback):
111
+ def __call__(self, request: pyramid.request.Request, feature: Feature, obj: Any) -> None:
112
+ from c2cgeoportal_commons.models.main import ( # pylint: disable=import-outside-toplevel
113
+ RestrictionArea,
114
+ )
115
+
116
+ assert models.DBSession is not None
117
+
118
+ geom = feature.geometry
119
+ if geom and not isinstance(geom, geojson.geometry.Default):
120
+ shape = shapely.geometry.shape(geom)
121
+ srid = Layers._get_geom_col_info(self.layer)[1]
122
+ spatial_elt = from_shape(shape, srid=srid)
123
+ allowed = self._get_geometry_check_base_query(request)
124
+ allowed = allowed.filter(
125
+ or_(RestrictionArea.area.is_(None), RestrictionArea.area.ST_Contains(spatial_elt))
126
+ )
127
+ if allowed.scalar() == 0:
128
+ raise HTTPForbidden()
129
+
130
+ # Check if geometry is valid
131
+ if Layers._get_validation_setting(self.layer, request):
132
+ Layers._validate_geometry(spatial_elt)
133
+
134
+ self.update(request, obj)
135
+
136
+
137
+ class _UpdateCallback(_BaseCallback):
138
+ def __call__(self, request: pyramid.request.Request, feature: Feature, obj: Any) -> None:
139
+ from c2cgeoportal_commons.models.main import ( # pylint: disable=import-outside-toplevel
140
+ RestrictionArea,
141
+ )
142
+
143
+ assert models.DBSession is not None
144
+
145
+ # we need both the "original" and "new" geometry to be
146
+ # within the restriction area
147
+ geom_attr, srid = Layers._get_geom_col_info(self.layer)
148
+ geom_attr = getattr(obj, geom_attr)
149
+ geom = feature.geometry
150
+ allowed = self._get_geometry_check_base_query(request)
151
+ allowed = allowed.filter(
152
+ or_(RestrictionArea.area.is_(None), RestrictionArea.area.ST_Contains(geom_attr))
153
+ )
154
+ spatial_elt = None
155
+ if geom and not isinstance(geom, geojson.geometry.Default):
156
+ shape = shapely.geometry.shape(geom)
157
+ spatial_elt = from_shape(shape, srid=srid)
158
+ allowed = allowed.filter(
159
+ or_(RestrictionArea.area.is_(None), RestrictionArea.area.ST_Contains(spatial_elt))
160
+ )
161
+ if allowed.scalar() == 0:
162
+ raise HTTPForbidden()
163
+
164
+ # Check is geometry is valid
165
+ if Layers._get_validation_setting(self.layer, request):
166
+ Layers._validate_geometry(spatial_elt)
167
+
168
+ self.update(request, obj)
169
+
170
+
171
+ class _DeleteCallback(_BaseCallback):
172
+ def __call__(self, request: pyramid.request.Request, obj: Any) -> None:
173
+ from c2cgeoportal_commons.models.main import ( # pylint: disable=import-outside-toplevel
174
+ RestrictionArea,
175
+ )
176
+
177
+ geom_attr = getattr(obj, Layers._get_geom_col_info(self.layer)[0])
178
+ allowed = self._get_geometry_check_base_query(request)
179
+ allowed = allowed.filter(
180
+ or_(RestrictionArea.area.is_(None), RestrictionArea.area.ST_Contains(geom_attr))
181
+ )
182
+ if allowed.scalar() == 0:
183
+ raise HTTPForbidden()
184
+
185
+
186
+ class Layers:
187
+ """
188
+ All the layers view (editing).
189
+
190
+ Mapfish protocol implementation
191
+ """
192
+
193
+ def __init__(self, request: pyramid.request.Request):
194
+ self.request = request
195
+ self.settings = self._get_settings(request)
196
+ self.layers_enum_config = self.settings.get("enum", {})
197
+
198
+ @staticmethod
199
+ def _get_settings(request: pyramid.request.Request) -> dict[str, Any]:
200
+ return cast(dict[str, Any], request.registry.settings.get("layers", {}))
201
+
202
+ @staticmethod
203
+ def _get_geom_col_info(layer: "main.Layer") -> tuple[str, int]:
204
+ """
205
+ Return information about the layer's geometry column.
206
+
207
+ Namely a ``(name, srid)`` tuple, where ``name`` is the name of the geometry column,
208
+ and ``srid`` its srid.
209
+
210
+ This function assumes that the names of geometry attributes in the mapped class are the same as those
211
+ of geometry columns.
212
+ """
213
+ mapped_class = get_layer_class(layer)
214
+ for p in class_mapper(mapped_class).iterate_properties:
215
+ if not isinstance(p, ColumnProperty):
216
+ continue
217
+ col = p.columns[0]
218
+ if isinstance(col.type, Geometry):
219
+ return col.name, col.type.srid
220
+ raise HTTPInternalServerError(f'Failed getting geometry column info for table "{layer.geo_table!s}".')
221
+
222
+ @staticmethod
223
+ def _get_layer(layer_id: int) -> "main.Layer":
224
+ """Return a ``Layer`` object for ``layer_id``."""
225
+ from c2cgeoportal_commons.models.main import Layer # pylint: disable=import-outside-toplevel
226
+
227
+ assert models.DBSession is not None
228
+
229
+ layer_id = int(layer_id)
230
+ try:
231
+ query = models.DBSession.query(Layer, Layer.geo_table)
232
+ query = query.filter(Layer.id == layer_id)
233
+ layer, geo_table = query.one()
234
+ except NoResultFound:
235
+ raise HTTPNotFound(f"Layer {layer_id:d} not found") from None
236
+ except MultipleResultsFound:
237
+ raise HTTPInternalServerError(f"Too many layers found with id {layer_id:d}") from None
238
+ if not geo_table:
239
+ raise HTTPNotFound(f"Layer {layer_id:d} has no geo table")
240
+ return cast("main.Layer", layer)
241
+
242
+ def _get_layers_for_request(self) -> Generator["main.Layer", None, None]:
243
+ """
244
+ Get a generator function that yields ``Layer`` objects.
245
+
246
+ Based on the layer ids found in the ``layer_id`` matchdict.
247
+ """
248
+ try:
249
+ layer_ids = (
250
+ int(layer_id) for layer_id in self.request.matchdict["layer_id"].split(",") if layer_id
251
+ )
252
+ for layer_id in layer_ids:
253
+ yield self._get_layer(layer_id)
254
+ except ValueError:
255
+ raise HTTPBadRequest( # pylint: disable=raise-missing-from
256
+ f"A Layer id in '{self.request.matchdict['layer_id']}' is not an integer"
257
+ )
258
+
259
+ def _get_layer_for_request(self) -> "main.Layer":
260
+ """Return a ``Layer`` object for the first layer id found in the ``layer_id`` matchdict."""
261
+ return next(self._get_layers_for_request())
262
+
263
+ def _get_protocol_for_layer(self, layer: "main.Layer") -> Protocol:
264
+ """Return a papyrus ``Protocol`` for the ``Layer`` object."""
265
+ cls = get_layer_class(layer)
266
+ geom_attr = self._get_geom_col_info(layer)[0]
267
+
268
+ return Protocol(
269
+ models.DBSession,
270
+ cls,
271
+ geom_attr,
272
+ before_insert=_InsertCallback(layer),
273
+ before_update=_UpdateCallback(layer),
274
+ before_delete=_DeleteCallback(layer),
275
+ )
276
+
277
+ def _get_protocol_for_request(self) -> Protocol:
278
+ """Return a papyrus ``Protocol`` for the first layer id found in the ``layer_id`` matchdict."""
279
+ layer = self._get_layer_for_request()
280
+ return self._get_protocol_for_layer(layer)
281
+
282
+ def _proto_read(self, layer: "main.Layer") -> FeatureCollection:
283
+ """Read features for the layer based on the self.request."""
284
+
285
+ from c2cgeoportal_commons.models.main import ( # pylint: disable=import-outside-toplevel
286
+ Layer,
287
+ RestrictionArea,
288
+ Role,
289
+ )
290
+
291
+ assert models.DBSession is not None
292
+
293
+ proto = self._get_protocol_for_layer(layer)
294
+ if layer.public:
295
+ return proto.read(self.request)
296
+ user = self.request.user
297
+ if user is None:
298
+ raise HTTPForbidden()
299
+ cls = proto.mapped_class
300
+ geom_attr = proto.geom_attr
301
+ ras = models.DBSession.query(RestrictionArea.area, RestrictionArea.area.ST_SRID())
302
+ ras = ras.join(RestrictionArea.roles)
303
+ ras = ras.join(RestrictionArea.layers)
304
+ ras = ras.filter(Role.id.in_(get_roles_id(self.request)))
305
+ ras = ras.filter(Layer.id == layer.id)
306
+ collect_ra = []
307
+ use_srid = -1
308
+ for ra, srid in ras.all():
309
+ if ra is None:
310
+ return proto.read(self.request)
311
+ use_srid = srid
312
+ collect_ra.append(to_shape(ra))
313
+ if not collect_ra:
314
+ raise HTTPForbidden()
315
+
316
+ filter1_ = create_filter(self.request, cls, geom_attr)
317
+ ra = unary_union(collect_ra)
318
+ filter2_ = func.ST_Contains(from_shape(ra, use_srid), getattr(cls, geom_attr))
319
+ filter_ = filter2_ if filter1_ is None else and_(filter1_, filter2_)
320
+
321
+ feature = proto.read(self.request, filter=filter_)
322
+ if isinstance(feature, HTTPException):
323
+ raise feature
324
+ return feature
325
+
326
+ @view_config(route_name="layers_read_many", renderer="geojson") # type: ignore
327
+ def read_many(self) -> FeatureCollection:
328
+ set_common_headers(self.request, "layers", Cache.PRIVATE_NO)
329
+
330
+ features = []
331
+ for layer in self._get_layers_for_request():
332
+ for f in self._proto_read(layer).features:
333
+ f.properties["__layer_id__"] = layer.id
334
+ features.append(f)
335
+
336
+ return FeatureCollection(features)
337
+
338
+ @view_config(route_name="layers_read_one", renderer="geojson") # type: ignore
339
+ def read_one(self) -> Feature:
340
+ from c2cgeoportal_commons.models.main import ( # pylint: disable=import-outside-toplevel
341
+ Layer,
342
+ RestrictionArea,
343
+ Role,
344
+ )
345
+
346
+ assert models.DBSession is not None
347
+
348
+ set_common_headers(self.request, "layers", Cache.PRIVATE_NO)
349
+
350
+ layer = self._get_layer_for_request()
351
+ protocol = self._get_protocol_for_layer(layer)
352
+ feature_id = self.request.matchdict.get("feature_id")
353
+ feature = protocol.read(self.request, id=feature_id)
354
+ if not isinstance(feature, Feature):
355
+ return feature
356
+ if layer.public:
357
+ return feature
358
+ if self.request.user is None:
359
+ raise HTTPForbidden()
360
+ geom = feature.geometry
361
+ if not geom or isinstance(geom, geojson.geometry.Default):
362
+ return feature
363
+ shape = shapely.geometry.shape(geom)
364
+ srid = self._get_geom_col_info(layer)[1]
365
+ spatial_elt = from_shape(shape, srid=srid)
366
+ allowed = models.DBSession.query(func.count(RestrictionArea.id)) # pylint: disable=not-callable
367
+ allowed = allowed.join(RestrictionArea.roles)
368
+ allowed = allowed.join(RestrictionArea.layers)
369
+ allowed = allowed.filter(Role.id.in_(get_roles_id(self.request)))
370
+ allowed = allowed.filter(Layer.id == layer.id)
371
+ allowed = allowed.filter(
372
+ or_(RestrictionArea.area.is_(None), RestrictionArea.area.ST_Contains(spatial_elt))
373
+ )
374
+ if allowed.scalar() == 0:
375
+ raise HTTPForbidden()
376
+
377
+ return feature
378
+
379
+ @view_config(route_name="layers_count", renderer="string") # type: ignore
380
+ def count(self) -> int:
381
+ set_common_headers(self.request, "layers", Cache.PRIVATE_NO)
382
+
383
+ protocol = self._get_protocol_for_request()
384
+ count = protocol.count(self.request)
385
+ if isinstance(count, HTTPException):
386
+ raise count
387
+ return cast(int, count)
388
+
389
+ @view_config(route_name="layers_create", renderer="geojson") # type: ignore
390
+ def create(self) -> FeatureCollection | None:
391
+ set_common_headers(self.request, "layers", Cache.PRIVATE_NO)
392
+
393
+ if self.request.user is None:
394
+ raise HTTPForbidden()
395
+
396
+ self.request.response.cache_control.no_cache = True
397
+
398
+ protocol = self._get_protocol_for_request()
399
+ try:
400
+ features = protocol.create(self.request)
401
+ if isinstance(features, HTTPException):
402
+ raise features
403
+ return features
404
+ except TopologicalError as e:
405
+ self.request.response.status_int = 400
406
+ return {"error_type": "validation_error", "message": str(e)}
407
+ except exc.IntegrityError as e:
408
+ _LOG.error(str(e))
409
+ assert e.orig is not None
410
+ self.request.response.status_int = 400
411
+ return {"error_type": "integrity_error", "message": str(e.orig.diag.message_primary)} # type: ignore[attr-defined]
412
+
413
+ @view_config(route_name="layers_update", renderer="geojson") # type: ignore
414
+ def update(self) -> Feature:
415
+ set_common_headers(self.request, "layers", Cache.PRIVATE_NO)
416
+
417
+ if self.request.user is None:
418
+ raise HTTPForbidden()
419
+
420
+ self.request.response.cache_control.no_cache = True
421
+
422
+ feature_id = self.request.matchdict.get("feature_id")
423
+ protocol = self._get_protocol_for_request()
424
+ try:
425
+ feature = protocol.update(self.request, feature_id)
426
+ if isinstance(feature, HTTPException):
427
+ raise feature
428
+ return cast(Feature, feature)
429
+ except TopologicalError as e:
430
+ self.request.response.status_int = 400
431
+ return {"error_type": "validation_error", "message": str(e)}
432
+ except exc.IntegrityError as e:
433
+ _LOG.error(str(e))
434
+ assert e.orig is not None
435
+ self.request.response.status_int = 400
436
+ return {"error_type": "integrity_error", "message": str(e.orig.diag.message_primary)} # type: ignore[attr-defined]
437
+
438
+ @staticmethod
439
+ def _validate_geometry(geom: geoalchemy2.elements.WKBElement | None) -> None:
440
+ assert models.DBSession is not None
441
+
442
+ if geom is not None:
443
+ simple = models.DBSession.query(func.ST_IsSimple(func.ST_GeomFromEWKB(geom))).scalar()
444
+ if not simple:
445
+ raise TopologicalError("Not simple")
446
+ valid = models.DBSession.query(func.ST_IsValid(func.ST_GeomFromEWKB(geom))).scalar()
447
+ if not valid:
448
+ reason = models.DBSession.query(func.ST_IsValidReason(func.ST_GeomFromEWKB(geom))).scalar()
449
+ raise TopologicalError(reason)
450
+
451
+ @staticmethod
452
+ def get_metadata(layer: "main.Layer", key: str, default: str | None = None) -> str | None:
453
+ metadata = layer.get_metadata(key)
454
+ if len(metadata) == 1:
455
+ metadata = metadata[0]
456
+ return metadata.value
457
+ return default
458
+
459
+ @classmethod
460
+ def _get_validation_setting(cls, layer: "main.Layer", request: pyramid.request.Request) -> bool:
461
+ # The validation UIMetadata is stored as a string, not a boolean
462
+ should_validate = cls.get_metadata(layer, "geometryValidation", None)
463
+ if should_validate:
464
+ return should_validate.lower() != "false"
465
+ return cast(bool, cls._get_settings(request).get("geometry_validation", False))
466
+
467
+ @view_config(route_name="layers_delete") # type: ignore
468
+ def delete(self) -> pyramid.response.Response:
469
+ if self.request.user is None:
470
+ raise HTTPForbidden()
471
+
472
+ feature_id = self.request.matchdict.get("feature_id")
473
+ protocol = self._get_protocol_for_request()
474
+ response = protocol.delete(self.request, feature_id)
475
+ if isinstance(response, HTTPException):
476
+ raise response
477
+ set_common_headers(self.request, "layers", Cache.PRIVATE_NO, response=response)
478
+ return response
479
+
480
+ @view_config(route_name="layers_metadata", renderer="xsd") # type: ignore
481
+ def metadata(self) -> pyramid.response.Response:
482
+ set_common_headers(self.request, "layers", Cache.PRIVATE)
483
+
484
+ layer = self._get_layer_for_request()
485
+ if not layer.public and self.request.user is None:
486
+ raise HTTPForbidden()
487
+
488
+ return get_layer_class(layer, with_last_update_columns=True)
489
+
490
+ @view_config(route_name="layers_enumerate_attribute_values", renderer="json") # type: ignore
491
+ def enumerate_attribute_values(self) -> dict[str, Any]:
492
+ set_common_headers(self.request, "layers", Cache.PUBLIC)
493
+
494
+ if self.layers_enum_config is None:
495
+ raise HTTPInternalServerError("Missing configuration")
496
+ layername = self.request.matchdict["layer_name"]
497
+ fieldname = self.request.matchdict["field_name"]
498
+ # TODO check if layer is public or not
499
+
500
+ return cast(dict[str, Any], self._enumerate_attribute_values(layername, fieldname))
501
+
502
+ @_CACHE_REGION.cache_on_arguments()
503
+ def _enumerate_attribute_values(self, layername: str, fieldname: str) -> dict[str, Any]:
504
+ if layername not in self.layers_enum_config:
505
+ raise HTTPBadRequest(f"Unknown layer: {layername!s}")
506
+
507
+ layerinfos = self.layers_enum_config[layername]
508
+ if fieldname not in layerinfos["attributes"]:
509
+ raise HTTPBadRequest(f"Unknown attribute: {fieldname!s}")
510
+ dbsession_name = layerinfos.get("dbsession", "dbsession")
511
+ dbsession = models.DBSessions.get(dbsession_name)
512
+ if dbsession is None:
513
+ raise HTTPInternalServerError(
514
+ f"No dbsession found for layer '{layername!s}' ({dbsession_name!s})"
515
+ )
516
+ values = sorted(self.query_enumerate_attribute_values(dbsession, layerinfos, fieldname))
517
+ enum = {"items": [{"value": value[0]} for value in values]}
518
+ return enum
519
+
520
+ @staticmethod
521
+ def query_enumerate_attribute_values(
522
+ dbsession: sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session],
523
+ layerinfos: dict[str, Any],
524
+ fieldname: str,
525
+ ) -> set[tuple[str, ...]]:
526
+ attrinfos = layerinfos["attributes"][fieldname]
527
+ table = attrinfos["table"]
528
+ layertable = get_table(table, session=dbsession())
529
+ column = attrinfos.get("column_name", fieldname)
530
+ attribute = getattr(layertable.columns, column)
531
+ # For instance if `separator` is a "," we consider that the column contains a
532
+ # comma separate list of values e.g.: "value1,value2".
533
+ if "separator" in attrinfos:
534
+ separator = attrinfos["separator"]
535
+ attribute = func.unnest(func.string_to_array(func.string_agg(attribute, separator), separator))
536
+ return set(cast(list[tuple[str, ...]], dbsession.query(attribute).order_by(attribute).all()))
537
+
538
+
539
+ def get_layer_class(layer: "main.Layer", with_last_update_columns: bool = False) -> type:
540
+ """
541
+ Get the SQLAlchemy class to edit a GeoMapFish layer.
542
+
543
+ Keyword Arguments:
544
+
545
+ layer: The GeoMapFish layer
546
+ with_last_update_columns: False to just have a class to access to the table and be able to
547
+ modify the last_update_columns, True to have a correct class to build the UI
548
+ (without the hidden column).
549
+
550
+ Returns: SQLAlchemy class
551
+ """
552
+
553
+ assert layer.geo_table is not None
554
+
555
+ # Exclude the columns used to record the last features update
556
+ exclude = [] if layer.exclude_properties is None else layer.exclude_properties.split(",")
557
+ if with_last_update_columns:
558
+ last_update_date = Layers.get_metadata(layer, "lastUpdateDateColumn")
559
+ if last_update_date is not None:
560
+ exclude.append(last_update_date)
561
+ last_update_user = Layers.get_metadata(layer, "lastUpdateUserColumn")
562
+ if last_update_user is not None:
563
+ exclude.append(last_update_user)
564
+
565
+ m = Layers.get_metadata(layer, "editingAttributesOrder")
566
+ attributes_order = m.split(",") if m else None
567
+ m = Layers.get_metadata(layer, "readonlyAttributes")
568
+ readonly_attributes = m.split(",") if m else None
569
+ m = Layers.get_metadata(layer, "editingEnumerations")
570
+ enumerations_config = json.loads(m) if m else None
571
+
572
+ primary_key = Layers.get_metadata(layer, "geotablePrimaryKey")
573
+ cls = get_class(
574
+ str(layer.geo_table.format(**os.environ)),
575
+ exclude_properties=exclude,
576
+ primary_key=primary_key,
577
+ attributes_order=attributes_order,
578
+ enumerations_config=enumerations_config,
579
+ readonly_attributes=readonly_attributes,
580
+ )
581
+
582
+ mapper = class_mapper(cls)
583
+ column_properties = [p.key for p in mapper.iterate_properties if isinstance(p, ColumnProperty)]
584
+
585
+ for attribute_name in attributes_order or []:
586
+ if attribute_name not in column_properties:
587
+ table = mapper.mapped_table
588
+ _LOG.warning(
589
+ 'Attribute "%s" does not exists in table "%s.%s".\n'
590
+ 'Please correct metadata "editingAttributesOrder" in layer "%s" (id=%s).\n'
591
+ "Available attributes are: %s.",
592
+ attribute_name,
593
+ table.schema,
594
+ table.name,
595
+ layer.name,
596
+ layer.id,
597
+ ", ".join(column_properties),
598
+ )
599
+
600
+ return cast(type, cls)
601
+
602
+
603
+ class ColumnProperties(TypedDict, total=False):
604
+ """Collected metadata information related to an editing attribute."""
605
+
606
+ name: str
607
+ type: str
608
+ nillable: bool
609
+ srid: int
610
+ enumeration: list[str]
611
+ restriction: str
612
+ maxLength: int # noqa
613
+ fractionDigits: int # noqa
614
+ totalDigits: int # noqa
615
+
616
+
617
+ def get_layer_metadata(layer: "main.Layer") -> list[ColumnProperties]:
618
+ """Get the metadata related to a layer."""
619
+
620
+ assert models.DBSession is not None
621
+
622
+ cls = get_layer_class(layer, with_last_update_columns=True)
623
+ edit_columns: list[ColumnProperties] = []
624
+
625
+ for column_property in class_mapper(cls).iterate_properties:
626
+ if isinstance(column_property, ColumnProperty):
627
+ if len(column_property.columns) != 1:
628
+ raise NotImplementedError
629
+
630
+ column = column_property.columns[0]
631
+
632
+ # Exclude columns that are primary keys
633
+ if not column.primary_key:
634
+ properties: ColumnProperties = _convert_column_type(column.type)
635
+ properties["name"] = column.key
636
+
637
+ if column.nullable:
638
+ properties["nillable"] = True
639
+ edit_columns.append(properties)
640
+ else:
641
+ for k, p in cls.__dict__.items():
642
+ if not isinstance(p, _AssociationProxy):
643
+ continue
644
+
645
+ relationship_property = class_mapper(cls).get_property(p.target)
646
+ target_cls = relationship_property.argument
647
+ query = models.DBSession.query(getattr(target_cls, p.value_attr))
648
+ properties = {}
649
+ if column.nullable:
650
+ properties["nillable"] = True
651
+
652
+ properties["name"] = k
653
+ properties["restriction"] = "enumeration"
654
+ properties["type"] = "xsd:string"
655
+ properties["enumeration"] = []
656
+ for value in query: # pylint: disable=not-an-iterable
657
+ properties["enumeration"].append(value[0])
658
+
659
+ edit_columns.append(properties)
660
+ return edit_columns
661
+
662
+
663
+ def _convert_column_type(column_type: object) -> ColumnProperties:
664
+ # SIMPLE_XSD_TYPES
665
+ for cls, xsd_type in XSDGenerator.SIMPLE_XSD_TYPES.items():
666
+ if isinstance(column_type, cls):
667
+ return {"type": xsd_type}
668
+
669
+ # Geometry type
670
+ if isinstance(column_type, Geometry):
671
+ geometry_type = column_type.geometry_type
672
+ if geometry_type in XSDGenerator.SIMPLE_GEOMETRY_XSD_TYPES:
673
+ xsd_type = XSDGenerator.SIMPLE_GEOMETRY_XSD_TYPES[geometry_type]
674
+ return {"type": xsd_type, "srid": int(column_type.srid)}
675
+ if geometry_type == "GEOMETRY":
676
+ xsd_type = "gml:GeometryPropertyType"
677
+ return {"type": xsd_type, "srid": int(column_type.srid)}
678
+
679
+ raise NotImplementedError(
680
+ f"The geometry type '{geometry_type}' is not supported, supported types: "
681
+ f"{','.join(XSDGenerator.SIMPLE_GEOMETRY_XSD_TYPES)}"
682
+ )
683
+
684
+ # Enumeration type
685
+ if isinstance(column_type, Enum):
686
+ restriction: ColumnProperties = {}
687
+ restriction["restriction"] = "enumeration"
688
+ restriction["type"] = "xsd:string"
689
+ restriction["enumeration"] = column_type.enums
690
+ return restriction
691
+
692
+ # String type
693
+ if isinstance(column_type, (String, Text, Unicode, UnicodeText)):
694
+ if column_type.length is None:
695
+ return {"type": "xsd:string"}
696
+ return {"type": "xsd:string", "maxLength": int(column_type.length)}
697
+
698
+ # Numeric Type
699
+ if isinstance(column_type, Numeric):
700
+ xsd_type2: ColumnProperties = {"type": "xsd:decimal"}
701
+ if column_type.scale is None and column_type.precision is None:
702
+ return xsd_type2
703
+
704
+ if column_type.scale is not None:
705
+ xsd_type2["fractionDigits"] = int(column_type.scale)
706
+ if column_type.precision is not None:
707
+ xsd_type2["totalDigits"] = int(column_type.precision)
708
+ return xsd_type2
709
+
710
+ raise NotImplementedError(
711
+ f"The type '{type(column_type).__name__}' is not supported, supported types: "
712
+ "Geometry, Enum, String, Text, Unicode, UnicodeText, Numeric"
713
+ )