c2cgeoportal-geoportal 2.3.5.80__py3-none-any.whl → 2.9rc45__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 +209 -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.9rc45.dist-info}/METADATA +21 -24
- c2cgeoportal_geoportal-2.9rc45.dist-info/RECORD +193 -0
- {c2cgeoportal_geoportal-2.3.5.80.dist-info → c2cgeoportal_geoportal-2.9rc45.dist-info}/WHEEL +1 -1
- c2cgeoportal_geoportal-2.9rc45.dist-info/entry_points.txt +28 -0
- c2cgeoportal_geoportal-2.9rc45.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,57 @@
|
|
1
|
+
# Copyright (c) 2011-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
|
+
|
29
|
+
import logging
|
30
|
+
import re
|
31
|
+
|
32
|
+
import pyramid.request
|
33
|
+
import pyramid.response
|
34
|
+
from pyramid.httpexceptions import HTTPFound
|
35
|
+
from pyramid.view import view_config
|
36
|
+
|
37
|
+
from c2cgeoportal_commons.lib.url import Url
|
38
|
+
from c2cgeoportal_geoportal.views.proxy import Proxy
|
39
|
+
|
40
|
+
logger = logging.getLogger(__name__)
|
41
|
+
|
42
|
+
|
43
|
+
class Dev(Proxy):
|
44
|
+
"""All the development views."""
|
45
|
+
|
46
|
+
THEME_RE = re.compile(r"/theme/.*$")
|
47
|
+
|
48
|
+
def __init__(self, request: pyramid.request.Request):
|
49
|
+
super().__init__(request)
|
50
|
+
self.dev_url = self.request.registry.settings["devserver_url"]
|
51
|
+
|
52
|
+
@view_config(route_name="dev") # type: ignore[misc]
|
53
|
+
def dev(self) -> pyramid.response.Response:
|
54
|
+
path = self.THEME_RE.sub("", self.request.path_info)
|
55
|
+
if self.request.path.endswith("/dynamic.js"):
|
56
|
+
return HTTPFound(location=self.request.route_url("dynamic", _query=self.request.params))
|
57
|
+
return self._proxy_response("dev", Url(f"{self.dev_url.rstrip('/')}/{path.lstrip('/')}"))
|
@@ -0,0 +1,209 @@
|
|
1
|
+
# Copyright (c) 2018-2025, 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
|
+
|
29
|
+
import re
|
30
|
+
import urllib.parse
|
31
|
+
from typing import Any, cast
|
32
|
+
|
33
|
+
import pyramid.request
|
34
|
+
from pyramid.httpexceptions import HTTPNotFound
|
35
|
+
from pyramid.view import view_config
|
36
|
+
from sqlalchemy import func
|
37
|
+
|
38
|
+
from c2cgeoportal_commons import models
|
39
|
+
from c2cgeoportal_commons.models import main
|
40
|
+
from c2cgeoportal_geoportal import is_allowed_host
|
41
|
+
from c2cgeoportal_geoportal.lib.cacheversion import get_cache_version
|
42
|
+
from c2cgeoportal_geoportal.lib.caching import get_region
|
43
|
+
from c2cgeoportal_geoportal.lib.common_headers import Cache, set_common_headers
|
44
|
+
|
45
|
+
CACHE_REGION = get_region("std")
|
46
|
+
|
47
|
+
|
48
|
+
class DynamicView:
|
49
|
+
"""The dynamic vies that provide the configuration of the client application."""
|
50
|
+
|
51
|
+
def __init__(self, request: pyramid.request.Request):
|
52
|
+
self.request = request
|
53
|
+
self.settings = request.registry.settings
|
54
|
+
self.interfaces_config = self.settings["interfaces_config"]
|
55
|
+
|
56
|
+
def get(self, value: dict[str, Any], interface: str) -> dict[str, Any]:
|
57
|
+
return cast(dict[str, Any], self.interfaces_config.get(interface, {}).get(value, {}))
|
58
|
+
|
59
|
+
@CACHE_REGION.cache_on_arguments()
|
60
|
+
def _fulltextsearch_groups(self) -> list[str]:
|
61
|
+
assert models.DBSession is not None
|
62
|
+
|
63
|
+
return [
|
64
|
+
group[0]
|
65
|
+
for group in models.DBSession.query(func.distinct(main.FullTextSearch.layer_name))
|
66
|
+
.filter(main.FullTextSearch.layer_name.isnot(None))
|
67
|
+
.all()
|
68
|
+
]
|
69
|
+
|
70
|
+
def _interface(
|
71
|
+
self,
|
72
|
+
interface_config: dict[str, Any],
|
73
|
+
interface_name: str,
|
74
|
+
original_interface_name: str,
|
75
|
+
dynamic: dict[str, Any],
|
76
|
+
) -> dict[str, Any]:
|
77
|
+
"""
|
78
|
+
Get the interface configuration.
|
79
|
+
|
80
|
+
Arguments:
|
81
|
+
|
82
|
+
interface_config: Current interface configuration
|
83
|
+
interface_name: Interface name (we use in the configuration)
|
84
|
+
original_interface_name: Original interface name (directly for the query string)
|
85
|
+
dynamic: The values that's dynamically generated
|
86
|
+
"""
|
87
|
+
|
88
|
+
if "extends" in interface_config:
|
89
|
+
constants = self._interface(
|
90
|
+
self.interfaces_config[interface_config["extends"]],
|
91
|
+
interface_name,
|
92
|
+
original_interface_name,
|
93
|
+
dynamic,
|
94
|
+
)
|
95
|
+
else:
|
96
|
+
constants = {}
|
97
|
+
|
98
|
+
constants.update(interface_config.get("constants", {}))
|
99
|
+
constants.update(
|
100
|
+
{
|
101
|
+
name: dynamic[value]
|
102
|
+
for name, value in interface_config.get("dynamic_constants", {}).items()
|
103
|
+
if value is not None
|
104
|
+
}
|
105
|
+
)
|
106
|
+
constants.update(
|
107
|
+
{
|
108
|
+
name: self.request.static_url(static_["name"]) + static_.get("append", "")
|
109
|
+
for name, static_ in interface_config.get("static", {}).items()
|
110
|
+
}
|
111
|
+
)
|
112
|
+
|
113
|
+
for constant, config in interface_config.get("routes", {}).items():
|
114
|
+
route_name = original_interface_name if config.get("currentInterface", False) else config["name"]
|
115
|
+
params: dict[str, str] = {}
|
116
|
+
params.update(config.get("params", {}))
|
117
|
+
for name, dyn in config.get("dynamic_params", {}).items():
|
118
|
+
params[name] = dynamic[dyn]
|
119
|
+
constants[constant] = self.request.route_url(
|
120
|
+
route_name, *config.get("elements", []), _query=params, **config.get("kw", {})
|
121
|
+
)
|
122
|
+
|
123
|
+
return constants
|
124
|
+
|
125
|
+
@view_config(route_name="dynamic", renderer="json") # type: ignore[misc]
|
126
|
+
def dynamic(self) -> dict[str, Any]:
|
127
|
+
is_allowed_host(self.request)
|
128
|
+
self.request.response.headers["Vary"] = "Host"
|
129
|
+
|
130
|
+
original_interface_name = self.request.params.get("interface")
|
131
|
+
interface_name = self.request.get_organization_interface(original_interface_name)
|
132
|
+
|
133
|
+
if interface_name not in self.interfaces_config:
|
134
|
+
interface_separator = "', '"
|
135
|
+
raise HTTPNotFound(
|
136
|
+
f"Interface '{interface_name}' doesn't exist in the 'interfaces_config', "
|
137
|
+
f"available interfaces are: '{interface_separator.join(self.interfaces_config.keys())}'."
|
138
|
+
)
|
139
|
+
|
140
|
+
interface_config = self.interfaces_config[interface_name]
|
141
|
+
lang_urls_suffix = interface_config.get("lang_urls_suffix", "")
|
142
|
+
|
143
|
+
i18next_configuration = self.settings.get("i18next", {})
|
144
|
+
i18next_configuration.setdefault("backend", {})
|
145
|
+
if "loadPath" not in i18next_configuration["backend"]:
|
146
|
+
path: list[str] = [
|
147
|
+
self.request.route_url("base").rstrip("/"),
|
148
|
+
"static-{{ns}}",
|
149
|
+
get_cache_version(),
|
150
|
+
"locales",
|
151
|
+
"{{lng}}.json",
|
152
|
+
]
|
153
|
+
i18next_configuration["backend"]["loadPath"] = "/".join(path)
|
154
|
+
|
155
|
+
dynamic = {
|
156
|
+
"interface": interface_name,
|
157
|
+
"cache_version": get_cache_version(),
|
158
|
+
"two_factor": self.request.registry.settings.get("authentication", {}).get("two_factor", False),
|
159
|
+
"lang_urls": {
|
160
|
+
lang: self.request.static_url(
|
161
|
+
f"/etc/geomapfish/static/{lang}{lang_urls_suffix}.json",
|
162
|
+
)
|
163
|
+
for lang in self.request.registry.settings["available_locale_names"]
|
164
|
+
},
|
165
|
+
"i18next_configuration": i18next_configuration,
|
166
|
+
"fulltextsearch_groups": self._fulltextsearch_groups(),
|
167
|
+
}
|
168
|
+
|
169
|
+
constants = self._interface(interface_config, interface_name, original_interface_name, dynamic)
|
170
|
+
|
171
|
+
do_redirect = False
|
172
|
+
url = None
|
173
|
+
if "redirect_interface" in interface_config:
|
174
|
+
no_redirect_query: dict[str, str | list[str]] = {"no_redirect": "t"}
|
175
|
+
if "query" in self.request.params:
|
176
|
+
query = urllib.parse.parse_qs(self.request.params["query"][1:], keep_blank_values=True)
|
177
|
+
no_redirect_query.update(query)
|
178
|
+
else:
|
179
|
+
query = {}
|
180
|
+
theme = None
|
181
|
+
if "path" in self.request.params:
|
182
|
+
match = re.match(".*/theme/(.*)", self.request.params["path"])
|
183
|
+
if match is not None:
|
184
|
+
theme = match.group(1)
|
185
|
+
if theme is not None:
|
186
|
+
no_redirect_url = self.request.route_url(
|
187
|
+
interface_config["redirect_interface"] + "theme", themes=theme, _query=no_redirect_query
|
188
|
+
)
|
189
|
+
url = self.request.route_url(
|
190
|
+
interface_config["redirect_interface"] + "theme", themes=theme, _query=query
|
191
|
+
).replace("+", "%20")
|
192
|
+
else:
|
193
|
+
no_redirect_url = self.request.route_url(
|
194
|
+
interface_config["redirect_interface"], _query=no_redirect_query
|
195
|
+
)
|
196
|
+
url = self.request.route_url(interface_config["redirect_interface"], _query=query).replace(
|
197
|
+
"+", "%20"
|
198
|
+
)
|
199
|
+
|
200
|
+
if "no_redirect" in query:
|
201
|
+
constants["redirectUrl"] = ""
|
202
|
+
else:
|
203
|
+
if interface_config.get("do_redirect", False):
|
204
|
+
do_redirect = True
|
205
|
+
else:
|
206
|
+
constants["redirectUrl"] = no_redirect_url
|
207
|
+
|
208
|
+
set_common_headers(self.request, "dynamic", Cache.PUBLIC_NO)
|
209
|
+
return {"constants": constants, "doRedirect": do_redirect, "redirectUrl": url}
|
@@ -0,0 +1,174 @@
|
|
1
|
+
# Copyright (c) 2011-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
|
+
|
29
|
+
import glob
|
30
|
+
import logging
|
31
|
+
import os
|
32
|
+
from typing import Any
|
33
|
+
|
34
|
+
import pyramid.request
|
35
|
+
from bs4 import BeautifulSoup
|
36
|
+
from pyramid.i18n import TranslationStringFactory
|
37
|
+
from pyramid.view import view_config
|
38
|
+
|
39
|
+
from c2cgeoportal_geoportal.lib.caching import get_region
|
40
|
+
from c2cgeoportal_geoportal.lib.common_headers import Cache, set_common_headers
|
41
|
+
|
42
|
+
_ = TranslationStringFactory("c2cgeoportal")
|
43
|
+
_LOG = logging.getLogger(__name__)
|
44
|
+
_CACHE_REGION = get_region("std")
|
45
|
+
|
46
|
+
|
47
|
+
class Entry:
|
48
|
+
"""All the entry points views."""
|
49
|
+
|
50
|
+
def __init__(self, request: pyramid.request.Request):
|
51
|
+
self.request = request
|
52
|
+
|
53
|
+
@view_config(route_name="testi18n", renderer="testi18n.html") # type: ignore[misc]
|
54
|
+
def testi18n(self) -> dict[str, Any]:
|
55
|
+
_ = self.request.translate
|
56
|
+
return {"title": _("title i18n")}
|
57
|
+
|
58
|
+
def get_ngeo_index_vars(self) -> dict[str, Any]:
|
59
|
+
set_common_headers(self.request, "index", Cache.PUBLIC_NO, content_type="text/html")
|
60
|
+
# Force urllogin to be converted to cookie when requesting the main HTML page
|
61
|
+
self.request.user # noqa
|
62
|
+
return {}
|
63
|
+
|
64
|
+
@staticmethod
|
65
|
+
@_CACHE_REGION.cache_on_arguments()
|
66
|
+
def get_apijs(api_filename: str, api_name: str | None) -> str:
|
67
|
+
with open(api_filename, encoding="utf-8") as api_file:
|
68
|
+
api = api_file.read().split("\n")
|
69
|
+
sourcemap = api.pop(-1)
|
70
|
+
if api_name:
|
71
|
+
api += [
|
72
|
+
f"if (window.{api_name} === undefined && window.geomapfishapp) {{",
|
73
|
+
f" window.{api_name} = window.geomapfishapp;",
|
74
|
+
"}",
|
75
|
+
]
|
76
|
+
api.append(sourcemap)
|
77
|
+
|
78
|
+
return "\n".join(api)
|
79
|
+
|
80
|
+
@view_config(route_name="apijs") # type: ignore[misc]
|
81
|
+
def apijs(self) -> pyramid.response.Response:
|
82
|
+
self.request.response.text = self.get_apijs(
|
83
|
+
self.request.registry.settings["static_files"]["api.js"],
|
84
|
+
self.request.registry.settings["api"].get("name"),
|
85
|
+
)
|
86
|
+
set_common_headers(self.request, "api", Cache.PUBLIC, content_type="application/javascript")
|
87
|
+
return self.request.response
|
88
|
+
|
89
|
+
def favicon(self) -> dict[str, Any]:
|
90
|
+
set_common_headers(self.request, "index", Cache.PUBLIC, content_type="image/vnd.microsoft.icon")
|
91
|
+
return {}
|
92
|
+
|
93
|
+
def robot_txt(self) -> dict[str, Any]:
|
94
|
+
set_common_headers(self.request, "index", Cache.PUBLIC, content_type="text/plain")
|
95
|
+
return {}
|
96
|
+
|
97
|
+
def apijsmap(self) -> dict[str, Any]:
|
98
|
+
set_common_headers(self.request, "api", Cache.PUBLIC, content_type="application/octet-stream")
|
99
|
+
return {}
|
100
|
+
|
101
|
+
def apicss(self) -> dict[str, Any]:
|
102
|
+
set_common_headers(self.request, "api", Cache.PUBLIC, content_type="text/css")
|
103
|
+
return {}
|
104
|
+
|
105
|
+
def apihelp(self) -> dict[str, Any]:
|
106
|
+
set_common_headers(self.request, "apihelp", Cache.PUBLIC)
|
107
|
+
return {}
|
108
|
+
|
109
|
+
|
110
|
+
def _get_ngeo_resources(pattern: str) -> list[str]:
|
111
|
+
"""Return the list of ngeo dist files matching the pattern."""
|
112
|
+
return glob.glob(f"/opt/c2cgeoportal/geoportal/node_modules/ngeo/dist/{pattern}")
|
113
|
+
|
114
|
+
|
115
|
+
def canvas_view(request: pyramid.request.Request, interface_config: dict[str, Any]) -> dict[str, Any]:
|
116
|
+
"""Get view used as entry point of a canvas interface."""
|
117
|
+
|
118
|
+
js_files = _get_ngeo_resources(f"{interface_config.get('layout', interface_config['name'])}*.js")
|
119
|
+
css_files = _get_ngeo_resources(f"{interface_config.get('layout', interface_config['name'])}*.css")
|
120
|
+
css = "\n ".join(
|
121
|
+
[
|
122
|
+
f'<link href="{request.static_url(css)}" rel="stylesheet" crossorigin="anonymous">'
|
123
|
+
for css in css_files
|
124
|
+
]
|
125
|
+
)
|
126
|
+
|
127
|
+
set_common_headers(request, "index", Cache.PUBLIC_NO, content_type="text/html")
|
128
|
+
|
129
|
+
spinner = ""
|
130
|
+
spinner_filenames = _get_ngeo_resources("spinner*.svg")
|
131
|
+
if spinner_filenames:
|
132
|
+
with open(spinner_filenames[0], encoding="utf-8") as spinner_file:
|
133
|
+
spinner = spinner_file.read()
|
134
|
+
|
135
|
+
return {
|
136
|
+
"request": request,
|
137
|
+
"header": f"""
|
138
|
+
<meta name="dynamicUrl" content="{request.route_url("dynamic")}">
|
139
|
+
<meta name="interface" content="{interface_config['name']}">
|
140
|
+
{css}""",
|
141
|
+
"footer": "\n ".join(
|
142
|
+
[f'<script src="{request.static_url(js)}" crossorigin="anonymous"></script>' for js in js_files]
|
143
|
+
),
|
144
|
+
"spinner": spinner,
|
145
|
+
}
|
146
|
+
|
147
|
+
|
148
|
+
def custom_view(
|
149
|
+
request: pyramid.request.Request, interface_config: dict[str, Any]
|
150
|
+
) -> pyramid.response.Response:
|
151
|
+
"""Get view used as entry point of a canvas interface."""
|
152
|
+
|
153
|
+
set_common_headers(request, "index", Cache.PUBLIC_NO, content_type="text/html")
|
154
|
+
|
155
|
+
html_filename = interface_config.get("html_filename", f"{interface_config['name']}.html")
|
156
|
+
if not html_filename.startswith("/"):
|
157
|
+
html_filename = os.path.join("/etc/static-frontend/", html_filename)
|
158
|
+
|
159
|
+
with open(html_filename, encoding="utf-8") as html_file:
|
160
|
+
html = BeautifulSoup(html_file, "html.parser")
|
161
|
+
|
162
|
+
meta = html.find("meta", attrs={"name": "interface"})
|
163
|
+
if meta is not None:
|
164
|
+
meta["content"] = interface_config["name"]
|
165
|
+
meta = html.find("meta", attrs={"name": "dynamicUrl"})
|
166
|
+
if meta is not None:
|
167
|
+
meta["content"] = request.route_url("dynamic")
|
168
|
+
|
169
|
+
if hasattr(request, "custom_interface_transformer"):
|
170
|
+
request.custom_interface_transformer(html, interface_config)
|
171
|
+
|
172
|
+
request.response.text = str(html)
|
173
|
+
|
174
|
+
return request.response
|
@@ -0,0 +1,189 @@
|
|
1
|
+
# Copyright (c) 2011-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
|
+
|
29
|
+
import re
|
30
|
+
|
31
|
+
import pyramid.request
|
32
|
+
from geoalchemy2.shape import to_shape
|
33
|
+
from geojson import Feature, FeatureCollection
|
34
|
+
from pyramid.httpexceptions import HTTPBadRequest, HTTPInternalServerError
|
35
|
+
from pyramid.view import view_config
|
36
|
+
from sqlalchemy import ColumnElement, and_, desc, func, or_
|
37
|
+
|
38
|
+
from c2cgeoportal_commons.models import DBSession
|
39
|
+
from c2cgeoportal_commons.models.main import FullTextSearch, Interface
|
40
|
+
from c2cgeoportal_geoportal import locale_negotiator
|
41
|
+
from c2cgeoportal_geoportal.lib.caching import get_region
|
42
|
+
from c2cgeoportal_geoportal.lib.common_headers import Cache, set_common_headers
|
43
|
+
from c2cgeoportal_geoportal.lib.fulltextsearch import Normalize
|
44
|
+
|
45
|
+
CACHE_REGION = get_region("std")
|
46
|
+
IGNORED_CHARS_RE = re.compile(r"[()&|!:<>\t]")
|
47
|
+
IGNORED_STARTUP_CHARS_RE = re.compile(r"^[']*")
|
48
|
+
|
49
|
+
|
50
|
+
class FullTextSearchView:
|
51
|
+
"""All the full-text search view."""
|
52
|
+
|
53
|
+
def __init__(self, request: pyramid.request.Request):
|
54
|
+
self.request = request
|
55
|
+
set_common_headers(request, "fulltextsearch", Cache.PUBLIC_NO)
|
56
|
+
self.settings = request.registry.settings.get("fulltextsearch", {})
|
57
|
+
self.languages = self.settings.get("languages", {})
|
58
|
+
self.fts_normalizer = Normalize(self.settings)
|
59
|
+
|
60
|
+
@staticmethod
|
61
|
+
@CACHE_REGION.cache_on_arguments()
|
62
|
+
def _get_interface_id(interface: str) -> int:
|
63
|
+
assert DBSession is not None
|
64
|
+
|
65
|
+
return DBSession.query(Interface).filter_by(name=interface).one().id
|
66
|
+
|
67
|
+
@view_config(route_name="fulltextsearch", renderer="geojson") # type: ignore[misc]
|
68
|
+
def fulltextsearch(self) -> FeatureCollection:
|
69
|
+
assert DBSession is not None
|
70
|
+
|
71
|
+
lang = locale_negotiator(self.request)
|
72
|
+
|
73
|
+
try:
|
74
|
+
language = self.languages[lang]
|
75
|
+
except KeyError:
|
76
|
+
return HTTPInternalServerError(detail=f"{lang!s} not defined in languages")
|
77
|
+
|
78
|
+
if "query" not in self.request.params:
|
79
|
+
return HTTPBadRequest(detail="no query")
|
80
|
+
terms = self.fts_normalizer(self.request.params.get("query"))
|
81
|
+
|
82
|
+
maxlimit = self.settings.get("maxlimit", 200)
|
83
|
+
|
84
|
+
try:
|
85
|
+
limit = int(self.request.params.get("limit", self.settings.get("defaultlimit", 30)))
|
86
|
+
except ValueError:
|
87
|
+
return HTTPBadRequest(detail="limit value is incorrect")
|
88
|
+
limit = min(limit, maxlimit)
|
89
|
+
|
90
|
+
try:
|
91
|
+
partitionlimit = int(self.request.params.get("partitionlimit", 0))
|
92
|
+
except ValueError:
|
93
|
+
return HTTPBadRequest(detail="partitionlimit value is incorrect")
|
94
|
+
partitionlimit = min(partitionlimit, maxlimit)
|
95
|
+
|
96
|
+
terms_array = [
|
97
|
+
IGNORED_STARTUP_CHARS_RE.sub("", elem) for elem in IGNORED_CHARS_RE.sub(" ", terms).split(" ")
|
98
|
+
]
|
99
|
+
terms_ts = "&".join(w + ":*" for w in terms_array if w != "")
|
100
|
+
_filter: ColumnElement[bool] = FullTextSearch.ts.op("@@")(func.to_tsquery(language, terms_ts))
|
101
|
+
|
102
|
+
if self.request.user is None:
|
103
|
+
_filter = and_(_filter, FullTextSearch.public.is_(True))
|
104
|
+
else:
|
105
|
+
_filter = and_(
|
106
|
+
_filter,
|
107
|
+
or_(
|
108
|
+
FullTextSearch.public.is_(True),
|
109
|
+
FullTextSearch.role_id.is_(None),
|
110
|
+
FullTextSearch.role_id.in_([r.id for r in self.request.user.roles]),
|
111
|
+
),
|
112
|
+
)
|
113
|
+
|
114
|
+
if "interface" in self.request.params:
|
115
|
+
_filter = and_(
|
116
|
+
_filter,
|
117
|
+
or_(
|
118
|
+
FullTextSearch.interface_id.is_(None),
|
119
|
+
FullTextSearch.interface_id == self._get_interface_id(self.request.params["interface"]),
|
120
|
+
),
|
121
|
+
)
|
122
|
+
else:
|
123
|
+
_filter = and_(_filter, FullTextSearch.interface_id.is_(None))
|
124
|
+
|
125
|
+
_filter = and_(_filter, or_(FullTextSearch.lang.is_(None), FullTextSearch.lang == lang))
|
126
|
+
|
127
|
+
rank_system = self.request.params.get("ranksystem")
|
128
|
+
if rank_system == "ts_rank_cd":
|
129
|
+
# The numbers used in ts_rank_cd() below indicate a normalization method.
|
130
|
+
# Several normalization methods can be combined using |.
|
131
|
+
# 2 divides the rank by the document length
|
132
|
+
# 8 divides the rank by the number of unique words in document
|
133
|
+
# By combining them, shorter results seem to be preferred over longer ones
|
134
|
+
# with the same ratio of matching words. But this relies only on testing it
|
135
|
+
# and on some assumptions about how it might be calculated
|
136
|
+
# (the normalization is applied two times with the combination of 2 and 8,
|
137
|
+
# so the effect on at least the one-word-results is therefore stronger).
|
138
|
+
rank = func.ts_rank_cd(FullTextSearch.ts, func.to_tsquery(language, terms_ts), 2 | 8)
|
139
|
+
else:
|
140
|
+
# Use similarity ranking system from module pg_trgm.
|
141
|
+
rank = func.similarity(FullTextSearch.label, terms)
|
142
|
+
|
143
|
+
if partitionlimit:
|
144
|
+
# Here we want to partition the search results based on
|
145
|
+
# layer_name and limit each partition.
|
146
|
+
row_number = (
|
147
|
+
func.row_number()
|
148
|
+
.over(partition_by=FullTextSearch.layer_name, order_by=(desc(rank), FullTextSearch.label))
|
149
|
+
.label("row_number")
|
150
|
+
)
|
151
|
+
sub_query = DBSession.query(FullTextSearch).add_columns(row_number).filter(_filter).subquery()
|
152
|
+
query = DBSession.query(
|
153
|
+
sub_query.c.id,
|
154
|
+
sub_query.c.label,
|
155
|
+
sub_query.c.params,
|
156
|
+
sub_query.c.layer_name,
|
157
|
+
sub_query.c.the_geom,
|
158
|
+
sub_query.c.actions,
|
159
|
+
)
|
160
|
+
query = query.filter(sub_query.c.row_number <= partitionlimit)
|
161
|
+
else:
|
162
|
+
query = DBSession.query(FullTextSearch).filter(_filter)
|
163
|
+
query = query.order_by(desc(rank))
|
164
|
+
query = query.order_by(FullTextSearch.label)
|
165
|
+
|
166
|
+
query = query.limit(limit)
|
167
|
+
objects = query.all()
|
168
|
+
|
169
|
+
features = []
|
170
|
+
for o in objects:
|
171
|
+
properties = {"label": o.label}
|
172
|
+
if o.layer_name is not None:
|
173
|
+
properties["layer_name"] = o.layer_name
|
174
|
+
if o.params is not None:
|
175
|
+
properties["params"] = o.params
|
176
|
+
if o.actions is not None:
|
177
|
+
properties["actions"] = o.actions
|
178
|
+
if o.actions is None and o.layer_name is not None:
|
179
|
+
properties["actions"] = [{"action": "add_layer", "data": o.layer_name}]
|
180
|
+
|
181
|
+
if o.the_geom is not None:
|
182
|
+
geom = to_shape(o.the_geom)
|
183
|
+
feature = Feature(id=o.id, geometry=geom, properties=properties, bbox=geom.bounds)
|
184
|
+
features.append(feature)
|
185
|
+
else:
|
186
|
+
feature = Feature(id=o.id, properties=properties)
|
187
|
+
features.append(feature)
|
188
|
+
|
189
|
+
return FeatureCollection(features)
|