c2cgeoportal-geoportal 2.9rc83__py3-none-any.whl → 2.9.0.352__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 +28 -8
- c2cgeoportal_geoportal/lib/__init__.py +1 -1
- c2cgeoportal_geoportal/lib/authentication.py +4 -1
- c2cgeoportal_geoportal/lib/bashcolor.py +1 -1
- c2cgeoportal_geoportal/lib/cacheversion.py +1 -1
- c2cgeoportal_geoportal/lib/caching.py +1 -1
- c2cgeoportal_geoportal/lib/check_collector.py +1 -1
- c2cgeoportal_geoportal/lib/checker.py +1 -1
- c2cgeoportal_geoportal/lib/dbreflection.py +1 -1
- c2cgeoportal_geoportal/lib/filter_capabilities.py +1 -1
- c2cgeoportal_geoportal/lib/fulltextsearch.py +1 -1
- c2cgeoportal_geoportal/lib/functionality.py +1 -1
- c2cgeoportal_geoportal/lib/headers.py +1 -1
- c2cgeoportal_geoportal/lib/i18n.py +1 -1
- c2cgeoportal_geoportal/lib/layers.py +1 -1
- c2cgeoportal_geoportal/lib/loader.py +1 -1
- c2cgeoportal_geoportal/lib/metrics.py +1 -1
- c2cgeoportal_geoportal/lib/oauth2.py +1 -1
- c2cgeoportal_geoportal/lib/oidc.py +109 -71
- c2cgeoportal_geoportal/lib/wmstparsing.py +1 -1
- c2cgeoportal_geoportal/lib/xsd.py +1 -1
- c2cgeoportal_geoportal/resources.py +1 -1
- c2cgeoportal_geoportal/scaffolds/advance_create/ci/config.yaml +1 -12
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Dockerfile +3 -3
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Makefile +1 -1
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/alembic.ini +2 -2
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/gunicorn.conf.py +1 -1
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/language_mapping +1 -0
- c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.apps.js +11 -1
- c2cgeoportal_geoportal/scaffolds/advance_update/cookiecutter.json +2 -0
- c2cgeoportal_geoportal/scaffolds/create/cookiecutter.json +2 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/main.yaml +1 -1
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/rebuild.yaml +1 -7
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Dockerfile +1 -1
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Makefile +4 -4
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/build +2 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/config.yaml +1 -8
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/requirements.txt +2 -2
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-lib.yaml +8 -4
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.default +3 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.project +1 -5
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/vars.yaml +2 -2
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/project.yaml +2 -0
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/db-backup +1 -1
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/db-restore +1 -1
- c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tilegeneration/config.yaml.tmpl +6 -4
- c2cgeoportal_geoportal/scaffolds/update/cookiecutter.json +2 -0
- c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/CONST_CHANGELOG.txt +14 -6
- c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_config-schema.yaml +2 -0
- c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_vars.yaml +16 -8
- c2cgeoportal_geoportal/scripts/__init__.py +1 -1
- c2cgeoportal_geoportal/scripts/c2cupgrade.py +2 -2
- c2cgeoportal_geoportal/scripts/create_demo_theme.py +1 -1
- c2cgeoportal_geoportal/scripts/manage_users.py +1 -1
- c2cgeoportal_geoportal/scripts/pcreate.py +11 -5
- c2cgeoportal_geoportal/scripts/theme2fts.py +141 -36
- c2cgeoportal_geoportal/scripts/urllogin.py +1 -1
- c2cgeoportal_geoportal/views/__init__.py +1 -1
- c2cgeoportal_geoportal/views/dev.py +1 -1
- c2cgeoportal_geoportal/views/entry.py +4 -2
- c2cgeoportal_geoportal/views/fulltextsearch.py +10 -4
- c2cgeoportal_geoportal/views/geometry_processing.py +1 -1
- c2cgeoportal_geoportal/views/i18n.py +1 -1
- c2cgeoportal_geoportal/views/layers.py +1 -1
- c2cgeoportal_geoportal/views/login.py +18 -8
- c2cgeoportal_geoportal/views/mapserverproxy.py +1 -1
- c2cgeoportal_geoportal/views/memory.py +1 -1
- c2cgeoportal_geoportal/views/ogcproxy.py +1 -1
- c2cgeoportal_geoportal/views/pdfreport.py +1 -1
- c2cgeoportal_geoportal/views/printproxy.py +1 -1
- c2cgeoportal_geoportal/views/profile.py +1 -1
- c2cgeoportal_geoportal/views/raster.py +1 -1
- c2cgeoportal_geoportal/views/resourceproxy.py +1 -1
- c2cgeoportal_geoportal/views/shortener.py +23 -7
- c2cgeoportal_geoportal/views/theme.py +18 -4
- c2cgeoportal_geoportal/views/tinyowsproxy.py +12 -6
- c2cgeoportal_geoportal/views/vector_tiles.py +1 -1
- {c2cgeoportal_geoportal-2.9rc83.dist-info → c2cgeoportal_geoportal-2.9.0.352.dist-info}/METADATA +7 -1
- {c2cgeoportal_geoportal-2.9rc83.dist-info → c2cgeoportal_geoportal-2.9.0.352.dist-info}/RECORD +93 -93
- tests/__init__.py +1 -1
- tests/test_cachebuster.py +1 -1
- tests/test_checker.py +1 -1
- tests/test_decimaljson.py +1 -1
- tests/test_headerstween.py +1 -1
- tests/test_init.py +1 -1
- tests/test_locale_negociator.py +1 -1
- tests/test_mapserverproxy_route_predicate.py +1 -1
- tests/test_raster.py +1 -1
- tests/test_wmstparsing.py +1 -1
- tests/xmlstr.py +1 -1
- {c2cgeoportal_geoportal-2.9rc83.dist-info → c2cgeoportal_geoportal-2.9.0.352.dist-info}/WHEEL +0 -0
- {c2cgeoportal_geoportal-2.9rc83.dist-info → c2cgeoportal_geoportal-2.9.0.352.dist-info}/entry_points.txt +0 -0
- {c2cgeoportal_geoportal-2.9rc83.dist-info → c2cgeoportal_geoportal-2.9.0.352.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2014-
|
|
1
|
+
# Copyright (c) 2014-2025, Camptocamp SA
|
|
2
2
|
# All rights reserved.
|
|
3
3
|
|
|
4
4
|
# Redistribution and use in source and binary forms, with or without
|
|
@@ -29,13 +29,14 @@
|
|
|
29
29
|
import gettext
|
|
30
30
|
import os
|
|
31
31
|
import sys
|
|
32
|
+
import time
|
|
32
33
|
from argparse import ArgumentParser, Namespace
|
|
33
34
|
from collections.abc import Iterator
|
|
34
35
|
from typing import TYPE_CHECKING, Any, Optional
|
|
35
36
|
|
|
36
37
|
import pyramid.config
|
|
38
|
+
import sqlalchemy.orm
|
|
37
39
|
import transaction
|
|
38
|
-
from sqlalchemy import func
|
|
39
40
|
from sqlalchemy.orm.session import Session
|
|
40
41
|
|
|
41
42
|
from c2cgeoportal_geoportal.lib.bashcolor import Color, colorize
|
|
@@ -89,6 +90,11 @@ def get_argparser() -> ArgumentParser:
|
|
|
89
90
|
"--no-layers", action="store_false", dest="layers", help="do not import the layers (tree leaf)"
|
|
90
91
|
)
|
|
91
92
|
parser.add_argument("--package", help="the application package")
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
"--stats",
|
|
95
|
+
action="store_true",
|
|
96
|
+
help="Print out statistics information",
|
|
97
|
+
)
|
|
92
98
|
fill_arguments(parser)
|
|
93
99
|
return parser
|
|
94
100
|
|
|
@@ -130,17 +136,8 @@ class Import:
|
|
|
130
136
|
else:
|
|
131
137
|
raise KeyError(KeyError(msg))
|
|
132
138
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
FullTextSearch,
|
|
136
|
-
Interface,
|
|
137
|
-
Role,
|
|
138
|
-
Theme,
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
self.session = session
|
|
142
|
-
self.session.execute(FullTextSearch.__table__.delete().where(FullTextSearch.from_theme))
|
|
143
|
-
|
|
139
|
+
print("Loading translations")
|
|
140
|
+
start_time = time.time()
|
|
144
141
|
self._: dict[str, gettext.NullTranslations] = {}
|
|
145
142
|
for lang in self.languages:
|
|
146
143
|
try:
|
|
@@ -152,13 +149,86 @@ class Import:
|
|
|
152
149
|
except OSError as e:
|
|
153
150
|
self._[lang] = gettext.NullTranslations()
|
|
154
151
|
print(f"Warning: {e} (language: {lang})")
|
|
152
|
+
if self.options.stats:
|
|
153
|
+
print(
|
|
154
|
+
f"Translations loaded in {time.time() - start_time:.2f} seconds "
|
|
155
|
+
f"({len(self.languages)} languages)"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
print("Loading the database")
|
|
159
|
+
# Must be done only once we have loaded the project config
|
|
160
|
+
from c2cgeoportal_commons.models.main import ( # pylint: disable=import-outside-toplevel
|
|
161
|
+
FullTextSearch,
|
|
162
|
+
Interface,
|
|
163
|
+
LayerGroup,
|
|
164
|
+
LayerWMS,
|
|
165
|
+
LayerWMTS,
|
|
166
|
+
Role,
|
|
167
|
+
Theme,
|
|
168
|
+
)
|
|
155
169
|
|
|
170
|
+
self.session = session
|
|
171
|
+
if self.options.stats:
|
|
172
|
+
print(
|
|
173
|
+
f"Session loaded in {time.time() - start_time:.2f} seconds "
|
|
174
|
+
f"({len(self.languages)} languages)"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
print("Delete the full-text search table")
|
|
178
|
+
start_time = time.time()
|
|
179
|
+
self.session.execute(FullTextSearch.__table__.delete().where(FullTextSearch.from_theme))
|
|
180
|
+
if self.options.stats:
|
|
181
|
+
print(
|
|
182
|
+
f"Deleted old entries in the full-text search table in "
|
|
183
|
+
f"{time.time() - start_time:.2f} seconds"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
print("Create cache")
|
|
187
|
+
start_time = time.time()
|
|
188
|
+
self._layerswms_cache = (
|
|
189
|
+
self.session.query(LayerWMS).options(sqlalchemy.orm.subqueryload(LayerWMS.metadatas)).all()
|
|
190
|
+
)
|
|
191
|
+
self._layerswmts_cache = (
|
|
192
|
+
self.session.query(LayerWMTS).options(sqlalchemy.orm.subqueryload(LayerWMTS.metadatas)).all()
|
|
193
|
+
)
|
|
194
|
+
self._layergroup_cache = (
|
|
195
|
+
self.session.query(LayerGroup)
|
|
196
|
+
.options(
|
|
197
|
+
sqlalchemy.orm.subqueryload(LayerGroup.children_relation),
|
|
198
|
+
sqlalchemy.orm.subqueryload(LayerWMS.metadatas),
|
|
199
|
+
)
|
|
200
|
+
.all()
|
|
201
|
+
)
|
|
202
|
+
all_themes = (
|
|
203
|
+
self.session.query(Theme)
|
|
204
|
+
.options(
|
|
205
|
+
sqlalchemy.orm.subqueryload(Theme.children_relation),
|
|
206
|
+
sqlalchemy.orm.subqueryload(LayerWMS.metadatas),
|
|
207
|
+
)
|
|
208
|
+
.all()
|
|
209
|
+
)
|
|
210
|
+
if self.options.stats:
|
|
211
|
+
print(
|
|
212
|
+
f"Cache created in {time.time() - start_time:.2f} seconds "
|
|
213
|
+
f"({len(self._layerswms_cache)} layerswms, "
|
|
214
|
+
f"{len(self._layerswmts_cache)} layerswmts, "
|
|
215
|
+
f"{len(self._layergroup_cache)} layergroups, "
|
|
216
|
+
f"{len(all_themes)} themes)"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
print("Loading interfaces")
|
|
220
|
+
start_time = time.time()
|
|
156
221
|
query = self.session.query(Interface)
|
|
157
222
|
if options.interfaces is not None:
|
|
158
223
|
query = query.filter(Interface.name.in_(options.interfaces))
|
|
159
224
|
else:
|
|
160
225
|
query = query.filter(Interface.name.notin_(options.exclude_interfaces))
|
|
161
226
|
self.interfaces = query.all()
|
|
227
|
+
if self.options.stats:
|
|
228
|
+
print(f"Loaded {len(self.interfaces)} interfaces in " f"{time.time() - start_time:.2f} seconds")
|
|
229
|
+
|
|
230
|
+
print("Collecting data")
|
|
231
|
+
start_time = time.time()
|
|
162
232
|
|
|
163
233
|
self.public_theme: dict[int, list[int]] = {}
|
|
164
234
|
self.public_group: dict[int, list[int]] = {}
|
|
@@ -166,13 +236,45 @@ class Import:
|
|
|
166
236
|
self.public_theme[interface.id] = []
|
|
167
237
|
self.public_group[interface.id] = []
|
|
168
238
|
|
|
239
|
+
self.full_text_search: list[dict[str, Any]] = []
|
|
240
|
+
|
|
169
241
|
for theme in self.session.query(Theme).filter_by(public=True).all():
|
|
170
242
|
self._add_theme(theme)
|
|
171
243
|
|
|
172
244
|
for role in self.session.query(Role).all():
|
|
173
|
-
for theme in
|
|
245
|
+
for theme in all_themes:
|
|
174
246
|
self._add_theme(theme, role)
|
|
175
247
|
|
|
248
|
+
if self.options.stats:
|
|
249
|
+
print(
|
|
250
|
+
f"Collected {len(self.full_text_search)} entries in "
|
|
251
|
+
f"{time.time() - start_time:.2f} seconds"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
print(f"Starting to fill the full-text search table with {len(self.full_text_search)} entries")
|
|
255
|
+
start_time = time.time()
|
|
256
|
+
self.session.execute(
|
|
257
|
+
sqlalchemy.insert(FullTextSearch).values(
|
|
258
|
+
{
|
|
259
|
+
"label": sqlalchemy.text(":label"),
|
|
260
|
+
"role_id": sqlalchemy.text(":role_id"),
|
|
261
|
+
"interface_id": sqlalchemy.text(":interface_id"),
|
|
262
|
+
"lang": sqlalchemy.text(":lang"),
|
|
263
|
+
"public": sqlalchemy.text(":public"),
|
|
264
|
+
"ts": sqlalchemy.text("to_tsvector(:fts_lang, :fts_content)"),
|
|
265
|
+
"actions": sqlalchemy.text("to_json(:actions)"),
|
|
266
|
+
"from_theme": sqlalchemy.text(":from_theme"),
|
|
267
|
+
}
|
|
268
|
+
),
|
|
269
|
+
self.full_text_search,
|
|
270
|
+
)
|
|
271
|
+
if self.options.stats:
|
|
272
|
+
print(
|
|
273
|
+
f"Filled the full-text search table in "
|
|
274
|
+
f"{time.time() - start_time:.2f} seconds "
|
|
275
|
+
f"({len(self.full_text_search)} entries)"
|
|
276
|
+
)
|
|
277
|
+
|
|
176
278
|
def _add_fts(
|
|
177
279
|
self,
|
|
178
280
|
item: "c2cgeoportal_commons.models.main.TreeItem",
|
|
@@ -180,8 +282,6 @@ class Import:
|
|
|
180
282
|
action: str,
|
|
181
283
|
role: Optional["c2cgeoportal_commons.models.main.Role"],
|
|
182
284
|
) -> None:
|
|
183
|
-
from c2cgeoportal_commons.models.main import FullTextSearch # pylint: disable=import-outside-toplevel
|
|
184
|
-
|
|
185
285
|
key = (
|
|
186
286
|
item.name if self.options.name else item.id,
|
|
187
287
|
interface.id,
|
|
@@ -190,22 +290,25 @@ class Import:
|
|
|
190
290
|
if key not in self.imported:
|
|
191
291
|
self.imported.add(key)
|
|
192
292
|
for lang in self.languages:
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
293
|
+
content = " ".join(
|
|
294
|
+
[
|
|
295
|
+
self.fts_normalizer(self._[lang].gettext(item.name)),
|
|
296
|
+
*[v.strip() for m in item.get_metadata("searchAlias") for v in m.value.split(",")],
|
|
297
|
+
]
|
|
298
|
+
)
|
|
299
|
+
self.full_text_search.append(
|
|
300
|
+
{
|
|
301
|
+
"label": self._render_label(item, lang),
|
|
302
|
+
"role_id": role.id if role is not None else None,
|
|
303
|
+
"interface_id": interface.id,
|
|
304
|
+
"lang": lang,
|
|
305
|
+
"public": role is None,
|
|
306
|
+
"fts_lang": self.fts_languages[lang],
|
|
307
|
+
"fts_content": content,
|
|
308
|
+
"actions": [{"action": action, "data": item.name}],
|
|
309
|
+
"from_theme": True,
|
|
310
|
+
}
|
|
205
311
|
)
|
|
206
|
-
fts.actions = [{"action": action, "data": item.name}]
|
|
207
|
-
fts.from_theme = True
|
|
208
|
-
self.session.add(fts)
|
|
209
312
|
|
|
210
313
|
def _add_theme(
|
|
211
314
|
self,
|
|
@@ -268,8 +371,13 @@ class Import:
|
|
|
268
371
|
|
|
269
372
|
@staticmethod
|
|
270
373
|
def _layer_visible(
|
|
271
|
-
layer: "c2cgeoportal_commons.models.main.Layer",
|
|
374
|
+
layer: "c2cgeoportal_commons.models.main.Layer",
|
|
375
|
+
role: Optional["c2cgeoportal_commons.models.main.Role"],
|
|
272
376
|
) -> bool:
|
|
377
|
+
if layer.public:
|
|
378
|
+
return True
|
|
379
|
+
if role is None:
|
|
380
|
+
return False
|
|
273
381
|
for restrictionarea in layer.restrictionareas:
|
|
274
382
|
if role in restrictionarea.roles:
|
|
275
383
|
return True
|
|
@@ -281,10 +389,7 @@ class Import:
|
|
|
281
389
|
interface: "c2cgeoportal_commons.models.main.Interface",
|
|
282
390
|
role: Optional["c2cgeoportal_commons.models.main.Role"],
|
|
283
391
|
) -> bool:
|
|
284
|
-
|
|
285
|
-
fill = layer.public and interface in layer.interfaces
|
|
286
|
-
else:
|
|
287
|
-
fill = interface in layer.interfaces and not layer.public and self._layer_visible(layer, role)
|
|
392
|
+
fill = interface in layer.interfaces and self._layer_visible(layer, role)
|
|
288
393
|
|
|
289
394
|
if fill and self.options.layers:
|
|
290
395
|
self._add_fts(layer, interface, "add_layer", role)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2011-
|
|
1
|
+
# Copyright (c) 2011-2025, Camptocamp SA
|
|
2
2
|
# All rights reserved.
|
|
3
3
|
|
|
4
4
|
# Redistribution and use in source and binary forms, with or without
|
|
@@ -148,9 +148,11 @@ def canvas_view(request: pyramid.request.Request, interface_config: dict[str, An
|
|
|
148
148
|
def custom_view(
|
|
149
149
|
request: pyramid.request.Request, interface_config: dict[str, Any]
|
|
150
150
|
) -> pyramid.response.Response:
|
|
151
|
-
"""Get view used as entry point of a canvas interface."""
|
|
151
|
+
"""Get view used as entry point of a canvas or custom interface."""
|
|
152
152
|
|
|
153
153
|
set_common_headers(request, "index", Cache.PUBLIC_NO, content_type="text/html")
|
|
154
|
+
# Force urllogin to be converted to cookie when requesting the main HTML page
|
|
155
|
+
request.user # noqa
|
|
154
156
|
|
|
155
157
|
html_filename = interface_config.get("html_filename", f"{interface_config['name']}.html")
|
|
156
158
|
if not html_filename.startswith("/"):
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2011-
|
|
1
|
+
# Copyright (c) 2011-2025, Camptocamp SA
|
|
2
2
|
# All rights reserved.
|
|
3
3
|
|
|
4
4
|
# Redistribution and use in source and binary forms, with or without
|
|
@@ -38,6 +38,7 @@ from sqlalchemy import ColumnElement, and_, desc, func, or_
|
|
|
38
38
|
from c2cgeoportal_commons.models import DBSession
|
|
39
39
|
from c2cgeoportal_commons.models.main import FullTextSearch, Interface
|
|
40
40
|
from c2cgeoportal_geoportal import locale_negotiator
|
|
41
|
+
from c2cgeoportal_geoportal.lib import get_roles_id
|
|
41
42
|
from c2cgeoportal_geoportal.lib.caching import get_region
|
|
42
43
|
from c2cgeoportal_geoportal.lib.common_headers import Cache, set_common_headers
|
|
43
44
|
from c2cgeoportal_geoportal.lib.fulltextsearch import Normalize
|
|
@@ -98,16 +99,21 @@ class FullTextSearchView:
|
|
|
98
99
|
]
|
|
99
100
|
terms_ts = "&".join(w + ":*" for w in terms_array if w != "")
|
|
100
101
|
_filter: ColumnElement[bool] = FullTextSearch.ts.op("@@")(func.to_tsquery(language, terms_ts))
|
|
101
|
-
|
|
102
102
|
if self.request.user is None:
|
|
103
|
-
_filter = and_(
|
|
103
|
+
_filter = and_(
|
|
104
|
+
_filter,
|
|
105
|
+
or_(
|
|
106
|
+
FullTextSearch.public.is_(True),
|
|
107
|
+
FullTextSearch.role_id.in_(get_roles_id(self.request)),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
104
110
|
else:
|
|
105
111
|
_filter = and_(
|
|
106
112
|
_filter,
|
|
107
113
|
or_(
|
|
108
114
|
FullTextSearch.public.is_(True),
|
|
109
115
|
FullTextSearch.role_id.is_(None),
|
|
110
|
-
FullTextSearch.role_id.in_(
|
|
116
|
+
FullTextSearch.role_id.in_(get_roles_id(self.request)),
|
|
111
117
|
),
|
|
112
118
|
)
|
|
113
119
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2011-
|
|
1
|
+
# Copyright (c) 2011-2025, Camptocamp SA
|
|
2
2
|
# All rights reserved.
|
|
3
3
|
|
|
4
4
|
# Redistribution and use in source and binary forms, with or without
|
|
@@ -81,6 +81,7 @@ class Login:
|
|
|
81
81
|
|
|
82
82
|
def _functionality(self) -> dict[str, list[str | int | float | bool | list[Any] | dict[str, Any]]]:
|
|
83
83
|
functionality = {}
|
|
84
|
+
|
|
84
85
|
for func_ in get_setting(self.settings, ("functionalities", "available_in_templates"), []):
|
|
85
86
|
functionality[func_] = get_functionality(func_, self.request, is_intranet(self.request))
|
|
86
87
|
return functionality
|
|
@@ -274,7 +275,7 @@ class Login:
|
|
|
274
275
|
f"is not the current host '{self.request.host}' "
|
|
275
276
|
f"or part of allowed_hosts: {', '.join(allowed_hosts)}"
|
|
276
277
|
)
|
|
277
|
-
_LOG.
|
|
278
|
+
_LOG.error(message)
|
|
278
279
|
return HTTPBadRequest(message)
|
|
279
280
|
|
|
280
281
|
if status == 302:
|
|
@@ -295,10 +296,11 @@ class Login:
|
|
|
295
296
|
if self.authentication_settings.get("openid_connect", {}).get("enabled", False):
|
|
296
297
|
client = oidc.get_oidc_client(self.request, self.request.host)
|
|
297
298
|
if hasattr(client, "revoke_token"):
|
|
298
|
-
|
|
299
|
-
client.revoke_token(
|
|
300
|
-
|
|
301
|
-
|
|
299
|
+
access_token = self.request.cookies.get("access_token")
|
|
300
|
+
client.revoke_token(access_token)
|
|
301
|
+
refresh_token = self.request.cookies.get("refresh_token")
|
|
302
|
+
if refresh_token is not None:
|
|
303
|
+
client.revoke_token(refresh_token)
|
|
302
304
|
|
|
303
305
|
headers = forget(self.request)
|
|
304
306
|
|
|
@@ -330,7 +332,8 @@ class Login:
|
|
|
330
332
|
if user is not None:
|
|
331
333
|
result.update(
|
|
332
334
|
{
|
|
333
|
-
"username": user.
|
|
335
|
+
"username": user.username,
|
|
336
|
+
"display_name": user.display_name,
|
|
334
337
|
"email": user.email,
|
|
335
338
|
"roles": [{"name": r.name, "id": r.id} for r in user.roles],
|
|
336
339
|
}
|
|
@@ -375,6 +378,10 @@ class Login:
|
|
|
375
378
|
_LOG.info("The login '%s' does not exist.", login)
|
|
376
379
|
raise HTTPUnauthorized("See server logs for details")
|
|
377
380
|
|
|
381
|
+
if user.deactivated:
|
|
382
|
+
_LOG.info("The login '%s' is deactivated.", login)
|
|
383
|
+
raise HTTPUnauthorized("See server logs for details")
|
|
384
|
+
|
|
378
385
|
if self.two_factor_auth:
|
|
379
386
|
if not self._validate_2fa_totp(user, otp):
|
|
380
387
|
_LOG.info("The second factor is wrong for user '%s'.", login)
|
|
@@ -638,6 +645,9 @@ class Login:
|
|
|
638
645
|
client = oidc.get_oidc_client(self.request, self.request.host)
|
|
639
646
|
assert models.DBSession is not None
|
|
640
647
|
|
|
648
|
+
if "code_verifier" not in self.request.cookies or "code_challenge" not in self.request.cookies:
|
|
649
|
+
raise HTTPBadRequest("Missing code verifier or code challenge cookies.")
|
|
650
|
+
|
|
641
651
|
token_response = client.authorization_code_flow.handle_authentication_result(
|
|
642
652
|
"?" + urllib.parse.urlencode(self.request.params),
|
|
643
653
|
code_verifier=self.request.cookies["code_verifier"],
|
|
@@ -670,7 +680,7 @@ class Login:
|
|
|
670
680
|
# TODO respect the user interface...
|
|
671
681
|
json.dumps(
|
|
672
682
|
{
|
|
673
|
-
"username": user.
|
|
683
|
+
"username": user.username,
|
|
674
684
|
"email": user.email,
|
|
675
685
|
"is_intranet": is_intranet(self.request),
|
|
676
686
|
"functionalities": self._functionality(),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2013-
|
|
1
|
+
# Copyright (c) 2013-2025, Camptocamp SA
|
|
2
2
|
# All rights reserved.
|
|
3
3
|
|
|
4
4
|
# Redistribution and use in source and binary forms, with or without
|
|
@@ -46,7 +46,7 @@ logger = logging.getLogger(__name__)
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
class Shortener:
|
|
49
|
-
"""All the views
|
|
49
|
+
"""All the views concern the shortener."""
|
|
50
50
|
|
|
51
51
|
def __init__(self, request: pyramid.request.Request):
|
|
52
52
|
self.request = request
|
|
@@ -55,8 +55,7 @@ class Shortener:
|
|
|
55
55
|
if "base_url" in self.settings:
|
|
56
56
|
self.short_bases.append(self.settings["base_url"])
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
def get(self) -> HTTPFound:
|
|
58
|
+
def _read(self) -> str:
|
|
60
59
|
assert DBSession is not None
|
|
61
60
|
|
|
62
61
|
ref = self.request.matchdict["ref"]
|
|
@@ -68,8 +67,19 @@ class Shortener:
|
|
|
68
67
|
short_urls[0].nb_hits += 1
|
|
69
68
|
short_urls[0].last_hit = datetime.now()
|
|
70
69
|
|
|
70
|
+
return short_urls[0].url
|
|
71
|
+
|
|
72
|
+
@view_config(route_name="shortener_get") # type: ignore[misc]
|
|
73
|
+
def get(self) -> HTTPFound:
|
|
74
|
+
long_url = self._read()
|
|
75
|
+
set_common_headers(self.request, "shortener", Cache.PUBLIC_NO)
|
|
76
|
+
return HTTPFound(location=long_url)
|
|
77
|
+
|
|
78
|
+
@view_config(route_name="shortener_fetch", renderer="json") # type: ignore[misc]
|
|
79
|
+
def fetch(self) -> dict[str, str]:
|
|
80
|
+
long_url = self._read()
|
|
71
81
|
set_common_headers(self.request, "shortener", Cache.PUBLIC_NO)
|
|
72
|
-
return
|
|
82
|
+
return {"long_url": long_url}
|
|
73
83
|
|
|
74
84
|
@view_config(route_name="shortener_create", renderer="json") # type: ignore[misc]
|
|
75
85
|
def create(self) -> dict[str, str]:
|
|
@@ -80,9 +90,15 @@ class Shortener:
|
|
|
80
90
|
|
|
81
91
|
url = self.request.params["url"]
|
|
82
92
|
|
|
93
|
+
url_length = len(url)
|
|
94
|
+
if "#" in url:
|
|
95
|
+
url_parsed = urlparse(url)
|
|
96
|
+
if url_parsed.fragment:
|
|
97
|
+
url_length -= len(url_parsed.fragment) + 1
|
|
98
|
+
|
|
83
99
|
# see: https://httpd.apache.org/docs/2.2/mod/core.html#limitrequestline
|
|
84
|
-
if
|
|
85
|
-
raise HTTPBadRequest(f"The parameter url is too long ({
|
|
100
|
+
if url_length > 8190:
|
|
101
|
+
raise HTTPBadRequest(f"The parameter url is too long ({url_length} > {8190})")
|
|
86
102
|
|
|
87
103
|
allowed_hosts = self.settings.get("allowed_hosts", [])
|
|
88
104
|
url_hostname, ok = is_allowed_url(self.request, url, allowed_hosts)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2011-
|
|
1
|
+
# Copyright (c) 2011-2025, Camptocamp SA
|
|
2
2
|
# All rights reserved.
|
|
3
3
|
|
|
4
4
|
# Redistribution and use in source and binary forms, with or without
|
|
@@ -402,7 +402,16 @@ class Theme:
|
|
|
402
402
|
mixed: bool = True,
|
|
403
403
|
) -> tuple[dict[str, Any] | None, set[str]]:
|
|
404
404
|
errors: set[str] = set()
|
|
405
|
-
layer_info = {
|
|
405
|
+
layer_info = {
|
|
406
|
+
"id": layer.id,
|
|
407
|
+
"name": layer.name,
|
|
408
|
+
"public": (
|
|
409
|
+
layer.public
|
|
410
|
+
or self.request.get_organization_role("anonymous")
|
|
411
|
+
in [r.name for ra in layer.restrictionareas for r in ra.roles]
|
|
412
|
+
),
|
|
413
|
+
"metadata": self._get_metadata_list(layer, errors),
|
|
414
|
+
}
|
|
406
415
|
if re.search("[/?#]", layer.name):
|
|
407
416
|
errors.add(f"The layer has an unsupported name '{layer.name}'.")
|
|
408
417
|
if layer.geo_table:
|
|
@@ -822,6 +831,11 @@ class Theme:
|
|
|
822
831
|
theme_theme = {
|
|
823
832
|
"id": theme.id,
|
|
824
833
|
"name": theme.name,
|
|
834
|
+
"public": (
|
|
835
|
+
theme.public
|
|
836
|
+
or self.request.get_organization_role("anonymous")
|
|
837
|
+
in [role.name for role in theme.restricted_roles]
|
|
838
|
+
),
|
|
825
839
|
"icon": icon,
|
|
826
840
|
"children": children,
|
|
827
841
|
"functionalities": self._get_functionalities(theme),
|
|
@@ -897,6 +911,7 @@ class Theme:
|
|
|
897
911
|
) -> tuple[Optional[etree.Element], set[str]]: # pylint: disable=c-extension-no-member
|
|
898
912
|
errors = set()
|
|
899
913
|
|
|
914
|
+
wfs_url = wfs_url.clone()
|
|
900
915
|
wfs_url.add_query(
|
|
901
916
|
{
|
|
902
917
|
"SERVICE": "WFS",
|
|
@@ -1278,8 +1293,7 @@ class Theme:
|
|
|
1278
1293
|
if not self.request.user:
|
|
1279
1294
|
raise pyramid.httpexceptions.HTTPForbidden()
|
|
1280
1295
|
|
|
1281
|
-
|
|
1282
|
-
if not admin_roles:
|
|
1296
|
+
if not self.request.has_permission("admin"):
|
|
1283
1297
|
raise pyramid.httpexceptions.HTTPForbidden()
|
|
1284
1298
|
|
|
1285
1299
|
self._ogc_server_clear_cache(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2015-
|
|
1
|
+
# Copyright (c) 2015-2025, Camptocamp SA
|
|
2
2
|
# All rights reserved.
|
|
3
3
|
|
|
4
4
|
# Redistribution and use in source and binary forms, with or without
|
|
@@ -30,6 +30,7 @@ import logging
|
|
|
30
30
|
from typing import Any
|
|
31
31
|
|
|
32
32
|
import pyramid.request
|
|
33
|
+
import sqlalchemy.exc
|
|
33
34
|
from defusedxml import ElementTree
|
|
34
35
|
from pyramid.httpexceptions import HTTPBadRequest, HTTPForbidden, HTTPInternalServerError, HTTPUnauthorized
|
|
35
36
|
from pyramid.view import view_config
|
|
@@ -59,11 +60,16 @@ class TinyOWSProxy(OGCProxy):
|
|
|
59
60
|
self.settings = request.registry.settings.get("tinyowsproxy", {})
|
|
60
61
|
|
|
61
62
|
assert "tinyows_url" in self.settings, "tinyowsproxy.tinyows_url must be set"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
try:
|
|
64
|
+
self.ogc_server = (
|
|
65
|
+
models.DBSession.query(main.OGCServer)
|
|
66
|
+
.filter(main.OGCServer.name == self.settings["ogc_server"])
|
|
67
|
+
.one()
|
|
68
|
+
)
|
|
69
|
+
except sqlalchemy.exc.NoResultFound:
|
|
70
|
+
raise HTTPBadRequest( # pylint: disable=raise-missing-from
|
|
71
|
+
f"OGC server {self.settings['ogc_server']} not found"
|
|
72
|
+
)
|
|
67
73
|
|
|
68
74
|
self.user = self.request.user
|
|
69
75
|
|