c2cgeoportal-geoportal 2.3.5.80__py3-none-any.whl → 2.9rc44__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- c2cgeoportal_geoportal/__init__.py +960 -0
- c2cgeoportal_geoportal/lib/__init__.py +256 -0
- c2cgeoportal_geoportal/lib/authentication.py +250 -0
- c2cgeoportal_geoportal/lib/bashcolor.py +46 -0
- c2cgeoportal_geoportal/lib/cacheversion.py +77 -0
- c2cgeoportal_geoportal/lib/caching.py +176 -0
- c2cgeoportal_geoportal/lib/check_collector.py +80 -0
- c2cgeoportal_geoportal/lib/checker.py +295 -0
- c2cgeoportal_geoportal/lib/common_headers.py +172 -0
- c2cgeoportal_geoportal/lib/dbreflection.py +266 -0
- c2cgeoportal_geoportal/lib/filter_capabilities.py +360 -0
- c2cgeoportal_geoportal/lib/fulltextsearch.py +50 -0
- c2cgeoportal_geoportal/lib/functionality.py +166 -0
- c2cgeoportal_geoportal/lib/headers.py +62 -0
- c2cgeoportal_geoportal/lib/i18n.py +38 -0
- c2cgeoportal_geoportal/lib/layers.py +132 -0
- c2cgeoportal_geoportal/lib/lingva_extractor.py +937 -0
- c2cgeoportal_geoportal/lib/loader.py +57 -0
- c2cgeoportal_geoportal/lib/metrics.py +117 -0
- c2cgeoportal_geoportal/lib/oauth2.py +1186 -0
- c2cgeoportal_geoportal/lib/oidc.py +304 -0
- c2cgeoportal_geoportal/lib/wmstparsing.py +353 -0
- c2cgeoportal_geoportal/lib/xsd.py +166 -0
- c2cgeoportal_geoportal/py.typed +0 -0
- c2cgeoportal_geoportal/resources.py +49 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/ci/config.yaml +26 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/cookiecutter.json +18 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/.dockerignore +6 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/.eslintrc.yaml +19 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/.prospector.yaml +30 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Dockerfile +75 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Makefile +6 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/alembic.ini +58 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/alembic.yaml +19 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/development.ini +121 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/gunicorn.conf.py +139 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/language_mapping +3 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/lingva-client.cfg +5 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/lingva-server.cfg +6 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/production.ini +38 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/requirements.txt +2 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/setup.py +25 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.api.js +41 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.apps.js +64 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.commons.js +11 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.config.js +22 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/__init__.py +42 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/authentication.py +10 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/dev.py +14 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/models.py +8 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/multi_organization.py +7 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/resources.py +11 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static-ngeo/api/index.js +12 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static-ngeo/js/{{cookiecutter.package}}module.js +25 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/subscribers.py +39 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/views/__init__.py +0 -0
- c2cgeoportal_geoportal/scaffolds/advance_update/cookiecutter.json +18 -0
- c2cgeoportal_geoportal/scaffolds/advance_update/{{cookiecutter.project}}/geoportal/CONST_Makefile +121 -0
- c2cgeoportal_geoportal/scaffolds/create/cookiecutter.json +18 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.dockerignore +14 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.editorconfig +17 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/main.yaml +73 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/rebuild.yaml +50 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/update_l10n.yaml +66 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.gitignore +16 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.pre-commit-config.yaml +35 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.prettierignore +1 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.prettierrc.yaml +2 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Dockerfile +75 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Makefile +70 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/README.rst +29 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/build +179 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/config.yaml +22 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/docker-compose-check +25 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/requirements.txt +2 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-db.yaml +24 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-lib.yaml +513 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-qgis.yaml +21 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose.override.sample.yaml +65 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose.yaml +121 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.default +102 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.project +69 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/vars.yaml +430 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/locale/en/LC_MESSAGES/{{cookiecutter.package}}_geoportal-client.po +6 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/css/desktop.css +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/css/iframe_api.css +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/css/mobile.css +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/banner_left.png +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/banner_right.png +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/blank.png +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/markers/marker-blue.png +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/markers/marker-gold.png +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/markers/marker-green.png +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/images/markers/marker.png +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/{{cookiecutter.package}}_geoportal/static/robot.txt.tmpl +3 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/data/Readme.txt +69 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/data/TM_EUROPE_BORDERS-0.3.sql +70 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/demo.map.tmpl +224 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Arial.ttf +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Arialbd.ttf +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Arialbi.ttf +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Ariali.ttf +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/NotoSans-Bold.ttf +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/NotoSans-BoldItalic.ttf +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/NotoSans-Italic.ttf +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/NotoSans-Regular.ttf +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Verdana.ttf +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Verdanab.ttf +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Verdanai.ttf +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts/Verdanaz.ttf +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/fonts.conf +12 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/mapserver.conf +16 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/mapserver.map.tmpl +87 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/mapserver/tinyows.xml.tmpl +36 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A3_Landscape.jrxml +207 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A3_Portrait.jrxml +185 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A4_Landscape.jrxml +200 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/A4_Portrait.jrxml +170 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/config.yaml.tmpl +175 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/legend.jrxml +109 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/localisation.properties +4 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/localisation_fr.properties +4 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/logo.png +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/north.svg +93 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/print/print-apps/{{cookiecutter.package}}/results.jrxml +25 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/project.yaml +18 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/pyproject.toml +7 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/qgisserver/pg_service.conf.tmpl +15 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/run_alembic.sh +11 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/db-backup +126 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/db-restore +132 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/setup.cfg +7 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/spell-ignore-words.txt +5 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tests/__init__.py +0 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tests/test_app.py +78 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tilegeneration/config.yaml.tmpl +195 -0
- c2cgeoportal_geoportal/scaffolds/update/cookiecutter.json +18 -0
- c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/.upgrade.yaml +67 -0
- c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/CONST_CHANGELOG.txt +304 -0
- c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/CONST_create_template/tests/test_testapp.py +48 -0
- c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/.CONST_vars.yaml.swp +0 -0
- c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_config-schema.yaml +927 -0
- c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_vars.yaml +1503 -0
- c2cgeoportal_geoportal/scripts/__init__.py +64 -0
- c2cgeoportal_geoportal/scripts/c2cupgrade.py +879 -0
- c2cgeoportal_geoportal/scripts/create_demo_theme.py +83 -0
- c2cgeoportal_geoportal/scripts/manage_users.py +140 -0
- c2cgeoportal_geoportal/scripts/pcreate.py +296 -0
- c2cgeoportal_geoportal/scripts/theme2fts.py +347 -0
- c2cgeoportal_geoportal/scripts/urllogin.py +81 -0
- c2cgeoportal_geoportal/templates/login.html +90 -0
- c2cgeoportal_geoportal/templates/notlogin.html +62 -0
- c2cgeoportal_geoportal/templates/testi18n.html +12 -0
- c2cgeoportal_geoportal/views/__init__.py +59 -0
- c2cgeoportal_geoportal/views/dev.py +57 -0
- c2cgeoportal_geoportal/views/dynamic.py +208 -0
- c2cgeoportal_geoportal/views/entry.py +174 -0
- c2cgeoportal_geoportal/views/fulltextsearch.py +189 -0
- c2cgeoportal_geoportal/views/geometry_processing.py +75 -0
- c2cgeoportal_geoportal/views/i18n.py +129 -0
- c2cgeoportal_geoportal/views/layers.py +713 -0
- c2cgeoportal_geoportal/views/login.py +684 -0
- c2cgeoportal_geoportal/views/mapserverproxy.py +234 -0
- c2cgeoportal_geoportal/views/memory.py +90 -0
- c2cgeoportal_geoportal/views/ogcproxy.py +120 -0
- c2cgeoportal_geoportal/views/pdfreport.py +245 -0
- c2cgeoportal_geoportal/views/printproxy.py +143 -0
- c2cgeoportal_geoportal/views/profile.py +192 -0
- c2cgeoportal_geoportal/views/proxy.py +261 -0
- c2cgeoportal_geoportal/views/raster.py +233 -0
- c2cgeoportal_geoportal/views/resourceproxy.py +73 -0
- c2cgeoportal_geoportal/views/shortener.py +152 -0
- c2cgeoportal_geoportal/views/theme.py +1322 -0
- c2cgeoportal_geoportal/views/tinyowsproxy.py +189 -0
- c2cgeoportal_geoportal/views/vector_tiles.py +83 -0
- {c2cgeoportal_geoportal-2.3.5.80.dist-info → c2cgeoportal_geoportal-2.9rc44.dist-info}/METADATA +21 -24
- c2cgeoportal_geoportal-2.9rc44.dist-info/RECORD +193 -0
- {c2cgeoportal_geoportal-2.3.5.80.dist-info → c2cgeoportal_geoportal-2.9rc44.dist-info}/WHEEL +1 -1
- c2cgeoportal_geoportal-2.9rc44.dist-info/entry_points.txt +28 -0
- c2cgeoportal_geoportal-2.9rc44.dist-info/top_level.txt +2 -0
- tests/__init__.py +100 -0
- tests/test_cachebuster.py +71 -0
- tests/test_caching.py +275 -0
- tests/test_checker.py +85 -0
- tests/test_decimaljson.py +47 -0
- tests/test_headerstween.py +64 -0
- tests/test_i18n.py +31 -0
- tests/test_init.py +193 -0
- tests/test_locale_negociator.py +69 -0
- tests/test_mapserverproxy_route_predicate.py +64 -0
- tests/test_raster.py +267 -0
- tests/test_wmstparsing.py +238 -0
- tests/xmlstr.py +103 -0
- c2cgeoportal_geoportal-2.3.5.80.dist-info/DESCRIPTION.rst +0 -8
- c2cgeoportal_geoportal-2.3.5.80.dist-info/RECORD +0 -7
- c2cgeoportal_geoportal-2.3.5.80.dist-info/entry_points.txt +0 -22
- c2cgeoportal_geoportal-2.3.5.80.dist-info/metadata.json +0 -1
- c2cgeoportal_geoportal-2.3.5.80.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[misc]
|
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[misc]
|
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[misc]
|
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[misc]
|
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[misc]
|
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[misc]
|
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[misc]
|
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[misc]
|
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
|
+
)
|