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.
Files changed (93) hide show
  1. c2cgeoportal_geoportal/__init__.py +28 -8
  2. c2cgeoportal_geoportal/lib/__init__.py +1 -1
  3. c2cgeoportal_geoportal/lib/authentication.py +4 -1
  4. c2cgeoportal_geoportal/lib/bashcolor.py +1 -1
  5. c2cgeoportal_geoportal/lib/cacheversion.py +1 -1
  6. c2cgeoportal_geoportal/lib/caching.py +1 -1
  7. c2cgeoportal_geoportal/lib/check_collector.py +1 -1
  8. c2cgeoportal_geoportal/lib/checker.py +1 -1
  9. c2cgeoportal_geoportal/lib/dbreflection.py +1 -1
  10. c2cgeoportal_geoportal/lib/filter_capabilities.py +1 -1
  11. c2cgeoportal_geoportal/lib/fulltextsearch.py +1 -1
  12. c2cgeoportal_geoportal/lib/functionality.py +1 -1
  13. c2cgeoportal_geoportal/lib/headers.py +1 -1
  14. c2cgeoportal_geoportal/lib/i18n.py +1 -1
  15. c2cgeoportal_geoportal/lib/layers.py +1 -1
  16. c2cgeoportal_geoportal/lib/loader.py +1 -1
  17. c2cgeoportal_geoportal/lib/metrics.py +1 -1
  18. c2cgeoportal_geoportal/lib/oauth2.py +1 -1
  19. c2cgeoportal_geoportal/lib/oidc.py +109 -71
  20. c2cgeoportal_geoportal/lib/wmstparsing.py +1 -1
  21. c2cgeoportal_geoportal/lib/xsd.py +1 -1
  22. c2cgeoportal_geoportal/resources.py +1 -1
  23. c2cgeoportal_geoportal/scaffolds/advance_create/ci/config.yaml +1 -12
  24. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Dockerfile +3 -3
  25. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/Makefile +1 -1
  26. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/alembic.ini +2 -2
  27. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/gunicorn.conf.py +1 -1
  28. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/language_mapping +1 -0
  29. c2cgeoportal_geoportal/scaffolds/advance_create/{{cookiecutter.project}}/geoportal/webpack.apps.js +11 -1
  30. c2cgeoportal_geoportal/scaffolds/advance_update/cookiecutter.json +2 -0
  31. c2cgeoportal_geoportal/scaffolds/create/cookiecutter.json +2 -0
  32. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/main.yaml +1 -1
  33. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/.github/workflows/rebuild.yaml +1 -7
  34. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Dockerfile +1 -1
  35. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/Makefile +4 -4
  36. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/build +2 -0
  37. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/config.yaml +1 -8
  38. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/ci/requirements.txt +2 -2
  39. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/docker-compose-lib.yaml +8 -4
  40. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.default +3 -0
  41. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/env.project +1 -5
  42. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/geoportal/vars.yaml +2 -2
  43. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/project.yaml +2 -0
  44. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/db-backup +1 -1
  45. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/scripts/db-restore +1 -1
  46. c2cgeoportal_geoportal/scaffolds/create/{{cookiecutter.project}}/tilegeneration/config.yaml.tmpl +6 -4
  47. c2cgeoportal_geoportal/scaffolds/update/cookiecutter.json +2 -0
  48. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/CONST_CHANGELOG.txt +14 -6
  49. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_config-schema.yaml +2 -0
  50. c2cgeoportal_geoportal/scaffolds/update/{{cookiecutter.project}}/geoportal/CONST_vars.yaml +16 -8
  51. c2cgeoportal_geoportal/scripts/__init__.py +1 -1
  52. c2cgeoportal_geoportal/scripts/c2cupgrade.py +2 -2
  53. c2cgeoportal_geoportal/scripts/create_demo_theme.py +1 -1
  54. c2cgeoportal_geoportal/scripts/manage_users.py +1 -1
  55. c2cgeoportal_geoportal/scripts/pcreate.py +11 -5
  56. c2cgeoportal_geoportal/scripts/theme2fts.py +141 -36
  57. c2cgeoportal_geoportal/scripts/urllogin.py +1 -1
  58. c2cgeoportal_geoportal/views/__init__.py +1 -1
  59. c2cgeoportal_geoportal/views/dev.py +1 -1
  60. c2cgeoportal_geoportal/views/entry.py +4 -2
  61. c2cgeoportal_geoportal/views/fulltextsearch.py +10 -4
  62. c2cgeoportal_geoportal/views/geometry_processing.py +1 -1
  63. c2cgeoportal_geoportal/views/i18n.py +1 -1
  64. c2cgeoportal_geoportal/views/layers.py +1 -1
  65. c2cgeoportal_geoportal/views/login.py +18 -8
  66. c2cgeoportal_geoportal/views/mapserverproxy.py +1 -1
  67. c2cgeoportal_geoportal/views/memory.py +1 -1
  68. c2cgeoportal_geoportal/views/ogcproxy.py +1 -1
  69. c2cgeoportal_geoportal/views/pdfreport.py +1 -1
  70. c2cgeoportal_geoportal/views/printproxy.py +1 -1
  71. c2cgeoportal_geoportal/views/profile.py +1 -1
  72. c2cgeoportal_geoportal/views/raster.py +1 -1
  73. c2cgeoportal_geoportal/views/resourceproxy.py +1 -1
  74. c2cgeoportal_geoportal/views/shortener.py +23 -7
  75. c2cgeoportal_geoportal/views/theme.py +18 -4
  76. c2cgeoportal_geoportal/views/tinyowsproxy.py +12 -6
  77. c2cgeoportal_geoportal/views/vector_tiles.py +1 -1
  78. {c2cgeoportal_geoportal-2.9rc83.dist-info → c2cgeoportal_geoportal-2.9.0.352.dist-info}/METADATA +7 -1
  79. {c2cgeoportal_geoportal-2.9rc83.dist-info → c2cgeoportal_geoportal-2.9.0.352.dist-info}/RECORD +93 -93
  80. tests/__init__.py +1 -1
  81. tests/test_cachebuster.py +1 -1
  82. tests/test_checker.py +1 -1
  83. tests/test_decimaljson.py +1 -1
  84. tests/test_headerstween.py +1 -1
  85. tests/test_init.py +1 -1
  86. tests/test_locale_negociator.py +1 -1
  87. tests/test_mapserverproxy_route_predicate.py +1 -1
  88. tests/test_raster.py +1 -1
  89. tests/test_wmstparsing.py +1 -1
  90. tests/xmlstr.py +1 -1
  91. {c2cgeoportal_geoportal-2.9rc83.dist-info → c2cgeoportal_geoportal-2.9.0.352.dist-info}/WHEEL +0 -0
  92. {c2cgeoportal_geoportal-2.9rc83.dist-info → c2cgeoportal_geoportal-2.9.0.352.dist-info}/entry_points.txt +0 -0
  93. {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-2023, Camptocamp SA
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
- # must be done only once we have loaded the project config
134
- from c2cgeoportal_commons.models.main import ( # pylint: disable=import-outside-toplevel
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 self.session.query(Theme).all():
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
- fts = FullTextSearch()
194
- fts.label = self._render_label(item, lang)
195
- fts.role = role
196
- fts.interface = interface
197
- fts.lang = lang
198
- fts.public = role is None
199
- fts.ts = func.to_tsvector(
200
- self.fts_languages[lang],
201
- " ".join(
202
- [self.fts_normalizer(self._[lang].gettext(item.name))]
203
- + [v.strip() for m in item.get_metadata("searchAlias") for v in m.value.split(",")]
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", role: "c2cgeoportal_commons.models.main.Role"
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
- if role is None:
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) 2012-2024, Camptocamp SA
1
+ # Copyright (c) 2012-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2023, Camptocamp SA
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
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2024, Camptocamp SA
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
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2024, Camptocamp SA
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-2024, Camptocamp SA
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_(_filter, FullTextSearch.public.is_(True))
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_([r.id for r in self.request.user.roles]),
116
+ FullTextSearch.role_id.in_(get_roles_id(self.request)),
111
117
  ),
112
118
  )
113
119
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2024, Camptocamp SA
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
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2019-2024, Camptocamp SA
1
+ # Copyright (c) 2019-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2012-2024, Camptocamp SA
1
+ # Copyright (c) 2012-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2024, Camptocamp SA
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.debug(message)
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
- user_info = json.loads(self.request.authenticated_userid)
299
- client.revoke_token(user_info["access_token"])
300
- if user_info.get("refresh_token") is not None:
301
- client.revoke_token(user_info["refresh_token"])
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.display_name,
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.display_name,
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) 2011-2024, Camptocamp SA
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
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2018-2024, Camptocamp SA
1
+ # Copyright (c) 2018-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2024, Camptocamp SA
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
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2024, Camptocamp SA
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
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2024, Camptocamp SA
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
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2012-2024, Camptocamp SA
1
+ # Copyright (c) 2012-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2012-2024, Camptocamp SA
1
+ # Copyright (c) 2012-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2011-2024, Camptocamp SA
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
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2013-2024, Camptocamp SA
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 conserne the shortener."""
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
- @view_config(route_name="shortener_get") # type: ignore[misc]
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 HTTPFound(location=short_urls[0].url)
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 len(url) > 8190:
85
- raise HTTPBadRequest(f"The parameter url is too long ({len(url)} > {8190})")
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-2024, Camptocamp SA
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 = {"id": layer.id, "name": layer.name, "metadata": self._get_metadata_list(layer, errors)}
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
- admin_roles = [r for r in self.request.user.roles if r.name == ("role_admin")]
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-2024, Camptocamp SA
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
- self.ogc_server = (
63
- models.DBSession.query(main.OGCServer)
64
- .filter(main.OGCServer.name == self.settings["ogc_server"])
65
- .one()
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
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2021-2024, Camptocamp SA
1
+ # Copyright (c) 2021-2025, Camptocamp SA
2
2
  # All rights reserved.
3
3
 
4
4
  # Redistribution and use in source and binary forms, with or without