c2cgeoportal-commons 2.6.0__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.

Potentially problematic release.


This version of c2cgeoportal-commons might be problematic. Click here for more details.

Files changed (88) hide show
  1. c2cgeoportal_commons/__init__.py +2 -5
  2. c2cgeoportal_commons/alembic/env.py +49 -33
  3. c2cgeoportal_commons/alembic/main/028477929d13_add_technical_roles.py +10 -7
  4. c2cgeoportal_commons/alembic/main/04f05bfbb05e_remove_the_old_is_expanded_column.py +62 -0
  5. c2cgeoportal_commons/alembic/main/116b9b79fc4d_internal_and_external_layer_tables_.py +42 -43
  6. c2cgeoportal_commons/alembic/main/1418cb05921b_merge_1_6_and_master_branches.py +9 -8
  7. c2cgeoportal_commons/alembic/main/164ac0819a61_add_image_format_to_wmts_layer.py +9 -6
  8. c2cgeoportal_commons/alembic/main/166ff2dcc48d_create_database.py +21 -24
  9. c2cgeoportal_commons/alembic/main/16e43f8c0330_remove_old_metadata_column.py +9 -6
  10. c2cgeoportal_commons/alembic/main/1d5d4abfebd1_add_restricted_theme.py +14 -8
  11. c2cgeoportal_commons/alembic/main/1de20166b274_remove_v1_artifacts.py +9 -6
  12. c2cgeoportal_commons/alembic/main/20137477bd02_update_icons_url.py +9 -6
  13. c2cgeoportal_commons/alembic/main/21f11066f8ec_trigger_on_role_updates_user_in_static.py +21 -20
  14. c2cgeoportal_commons/alembic/main/22e6dfb556de_add_description_tree_.py +9 -6
  15. c2cgeoportal_commons/alembic/main/29f2a32859ec_merge_1_6_and_master_branches.py +9 -8
  16. c2cgeoportal_commons/alembic/main/2b8ed8c1df94_set_layergroup_treeitem_is_as_a_primary_.py +9 -6
  17. c2cgeoportal_commons/alembic/main/2e57710fecfe_update_the_ogc_server_for_ogc_api.py +71 -0
  18. c2cgeoportal_commons/alembic/main/32527659d57b_move_exclude_properties_from_layerv1_to_.py +15 -12
  19. c2cgeoportal_commons/alembic/main/32b21aa1d0ed_merge_e004f76e951a_and_e004f76e951a_.py +9 -8
  20. c2cgeoportal_commons/alembic/main/338b57593823_remove_trigger_on_role_name_change.py +25 -24
  21. c2cgeoportal_commons/alembic/main/415746eb9f6_changes_for_v2.py +45 -43
  22. c2cgeoportal_commons/alembic/main/44c91d82d419_add_table_log.py +72 -0
  23. c2cgeoportal_commons/alembic/main/5109242131ce_add_column_time_widget.py +11 -9
  24. c2cgeoportal_commons/alembic/main/52916d8fde8b_add_sql_fields_to_vector_tiles.py +60 -0
  25. c2cgeoportal_commons/alembic/main/53ba1a68d5fe_add_theme_to_fulltextsearch.py +9 -6
  26. c2cgeoportal_commons/alembic/main/54645a535ad6_add_ordering_in_relation.py +17 -14
  27. c2cgeoportal_commons/alembic/main/56dc90838d90_fix_removing_layerv1.py +10 -8
  28. c2cgeoportal_commons/alembic/main/596ba21e3833_separate_local_internal.py +13 -14
  29. c2cgeoportal_commons/alembic/main/678f88c7ad5e_add_vector_tiles_layers_table.py +9 -6
  30. c2cgeoportal_commons/alembic/main/6a412d9437b1_rename_serverogc_to_ogcserver.py +9 -6
  31. c2cgeoportal_commons/alembic/main/6d87fdad275a_convert_metadata_to_the_right_case.py +13 -20
  32. c2cgeoportal_commons/alembic/main/7530011a66a7_trigger_on_role_updates_user_in_static.py +21 -20
  33. c2cgeoportal_commons/alembic/main/78fd093c8393_add_api_s_intrfaces.py +36 -52
  34. c2cgeoportal_commons/alembic/main/7d271f4527cd_add_layer_column_in_layerv1_table.py +9 -6
  35. c2cgeoportal_commons/alembic/main/809650bd04c3_add_dimension_field.py +9 -6
  36. c2cgeoportal_commons/alembic/main/8117bb9bba16_use_dimension_on_all_the_layers.py +9 -6
  37. c2cgeoportal_commons/alembic/main/87f8330ed64e_add_missing_delete_cascades.py +9 -6
  38. c2cgeoportal_commons/alembic/main/9268a1dffac0_add_trigger_to_be_able_to_correctly_.py +11 -8
  39. c2cgeoportal_commons/alembic/main/94db7e7e5b21_merge_2_2_and_master_branches.py +9 -8
  40. c2cgeoportal_commons/alembic/main/951ff84bd8ec_be_able_to_delete_a_wms_layer_in_sql.py +9 -6
  41. c2cgeoportal_commons/alembic/main/a00109812f89_add_field_layer_wms_valid.py +11 -8
  42. c2cgeoportal_commons/alembic/main/a4558f032d7d_add_support_of_cog_layers.py +74 -0
  43. c2cgeoportal_commons/alembic/main/a4f1aac9bda_merge_1_6_and_master_branches.py +9 -8
  44. c2cgeoportal_commons/alembic/main/b60f2a505f42_remame_uimetadata_to_metadata.py +9 -6
  45. c2cgeoportal_commons/alembic/main/b6b09f414fe8_sync_model_database.py +165 -0
  46. c2cgeoportal_commons/alembic/main/c75124553bf3_remove_deprecated_columns.py +9 -6
  47. c2cgeoportal_commons/alembic/main/d48a63b348f1_change_mapserver_url_for_docker.py +13 -14
  48. c2cgeoportal_commons/alembic/main/d8ef99bc227e_be_able_to_delete_a_linked_functionality.py +15 -12
  49. c2cgeoportal_commons/alembic/main/daf738d5bae4_merge_2_0_and_master_branches.py +9 -8
  50. c2cgeoportal_commons/alembic/main/dba87f2647f9_merge_2_2_on_2_3.py +9 -8
  51. c2cgeoportal_commons/alembic/main/e004f76e951a_add_missing_not_null.py +15 -32
  52. c2cgeoportal_commons/alembic/main/e7e03dedade3_put_the_default_wms_server_in_the_.py +13 -14
  53. c2cgeoportal_commons/alembic/main/e85afd327ab3_cascade_deletes_to_tsearch.py +9 -6
  54. c2cgeoportal_commons/alembic/main/ec82a8906649_add_missing_on_delete_cascade_on_layer_.py +13 -10
  55. c2cgeoportal_commons/alembic/main/ee25d267bf46_main_interface_desktop.py +11 -14
  56. c2cgeoportal_commons/alembic/main/eeb345672454_merge_2_4_and_master_branches.py +9 -8
  57. c2cgeoportal_commons/alembic/static/0c640a58a09a_add_opt_key_column.py +9 -6
  58. c2cgeoportal_commons/alembic/static/107b81f5b9fe_add_missing_delete_cascades.py +9 -6
  59. c2cgeoportal_commons/alembic/static/1857owc78a07_add_last_login_and_expiration_date.py +7 -6
  60. c2cgeoportal_commons/alembic/static/1da396a88908_move_user_table_to_static_schema.py +19 -20
  61. c2cgeoportal_commons/alembic/static/267b4c1bde2e_add_display_name_in_the_user_for_better_.py +62 -0
  62. c2cgeoportal_commons/alembic/static/3f89a7d71a5e_alter_column_url_to_remove_limitation.py +8 -7
  63. c2cgeoportal_commons/alembic/static/44c91d82d419_add_table_log.py +72 -0
  64. c2cgeoportal_commons/alembic/static/53d671b17b20_add_timezone_on_datetime_fields.py +9 -6
  65. c2cgeoportal_commons/alembic/static/5472fbc19f39_add_temp_password_column.py +9 -6
  66. c2cgeoportal_commons/alembic/static/76d72fb3fcb9_add_oauth2_pkce.py +70 -0
  67. c2cgeoportal_commons/alembic/static/7ef947f30f20_add_oauth2_tables.py +9 -6
  68. c2cgeoportal_commons/alembic/static/910b4ca53b68_sync_model_database.py +186 -0
  69. c2cgeoportal_commons/alembic/static/aa41e9613256_wip_add_openid_connect_support.py +64 -0
  70. c2cgeoportal_commons/alembic/static/ae5e88f35669_add_table_user_role.py +15 -18
  71. c2cgeoportal_commons/alembic/static/bd029dbfc11a_fill_tech_data_column.py +10 -8
  72. c2cgeoportal_commons/lib/email_.py +14 -16
  73. c2cgeoportal_commons/lib/literal.py +44 -0
  74. c2cgeoportal_commons/lib/url.py +164 -58
  75. c2cgeoportal_commons/lib/validators.py +2 -4
  76. c2cgeoportal_commons/models/__init__.py +19 -15
  77. c2cgeoportal_commons/models/main.py +1191 -263
  78. c2cgeoportal_commons/models/sqlalchemy.py +14 -18
  79. c2cgeoportal_commons/models/static.py +305 -78
  80. c2cgeoportal_commons/py.typed +0 -0
  81. c2cgeoportal_commons/testing/__init__.py +17 -12
  82. c2cgeoportal_commons/testing/initializedb.py +15 -14
  83. {c2cgeoportal_commons-2.6.0.dist-info → c2cgeoportal_commons-2.9rc45.dist-info}/METADATA +15 -16
  84. c2cgeoportal_commons-2.9rc45.dist-info/RECORD +89 -0
  85. {c2cgeoportal_commons-2.6.0.dist-info → c2cgeoportal_commons-2.9rc45.dist-info}/WHEEL +1 -1
  86. tests/conftest.py +1 -3
  87. c2cgeoportal_commons-2.6.0.dist-info/RECORD +0 -76
  88. {c2cgeoportal_commons-2.6.0.dist-info → c2cgeoportal_commons-2.9rc45.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,4 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- # Copyright (c) 2012-2021, Camptocamp SA
1
+ # Copyright (c) 2012-2024, Camptocamp SA
4
2
  # All rights reserved.
5
3
 
6
4
  # Redistribution and use in source and binary forms, with or without
@@ -27,7 +25,7 @@
27
25
  # of the authors and should not be interpreted as representing official policies,
28
26
  # either expressed or implied, of the FreeBSD Project.
29
27
  import json
30
- from typing import Any, Optional, Type
28
+ from typing import Any
31
29
 
32
30
  from sqlalchemy.engine import Dialect
33
31
  from sqlalchemy.types import VARCHAR, TypeDecorator, UserDefinedType
@@ -35,37 +33,35 @@ from sqlalchemy.types import VARCHAR, TypeDecorator, UserDefinedType
35
33
 
36
34
  # get from https://docs.sqlalchemy.org/en/latest/orm/extensions/
37
35
  # mutable.html#establishing-mutability-on-scalar-column-values
38
- class JSONEncodedDict(TypeDecorator):
39
- """
40
- Represents an immutable structure as a json-encoded string.
41
- """
36
+ class JSONEncodedDict(TypeDecorator[Any]):
37
+ """Represent an immutable structure as a json-encoded string."""
42
38
 
43
39
  impl = VARCHAR
44
40
 
45
- @staticmethod
46
- def process_bind_param(value: Optional[dict], _: Dialect) -> Optional[str]:
41
+ def process_bind_param(self, value: dict[str, Any] | None, _: Dialect) -> str | None:
47
42
  return json.dumps(value) if value is not None else None
48
43
 
49
- @staticmethod
50
- def process_result_value(value: Optional[str], _: Dialect) -> Optional[dict]:
44
+ def process_result_value(self, value: str | None, _: Dialect) -> dict[str, Any] | None:
51
45
  return json.loads(value) if value is not None else None
52
46
 
53
47
  @property
54
- def python_type(self) -> Type:
48
+ def python_type(self) -> type[Any]:
55
49
  return dict
56
50
 
57
- @staticmethod
58
- def process_literal_param(value: str, dialect: Any) -> str:
51
+ def process_literal_param(self, value: Any | None, dialect: Any) -> str:
52
+ assert isinstance(value, str)
59
53
  del dialect
60
54
  return json.dumps(value)
61
55
 
62
56
 
63
- class TsVector(UserDefinedType):
57
+ class TsVector(UserDefinedType[dict[str, str]]): # pylint: disable=abstract-method
64
58
  """A custom type for PostgreSQL's tsvector type."""
65
59
 
66
- def get_col_spec(self) -> str: # pylint: disable=no-self-use
60
+ cache_ok = True
61
+
62
+ def get_col_spec(self) -> str:
67
63
  return "TSVECTOR"
68
64
 
69
65
  @property
70
- def python_type(self) -> Type:
66
+ def python_type(self) -> type[Any]:
71
67
  return dict
@@ -1,6 +1,4 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- # Copyright (c) 2011-2021, Camptocamp SA
1
+ # Copyright (c) 2011-2024, Camptocamp SA
4
2
  # All rights reserved.
5
3
 
6
4
  # Redistribution and use in source and binary forms, with or without
@@ -30,31 +28,34 @@
30
28
 
31
29
  import crypt
32
30
  import logging
33
- from datetime import datetime
31
+ import os
32
+ from datetime import datetime, timezone
34
33
  from hashlib import sha1
35
34
  from hmac import compare_digest as compare_hash
36
- from typing import Any, List
35
+ from typing import Any
37
36
 
38
- import pytz
39
37
  import sqlalchemy.schema
40
38
  from c2c.template.config import config
41
39
  from sqlalchemy import Column, ForeignKey, Table
42
40
  from sqlalchemy.dialects.postgresql import HSTORE
43
41
  from sqlalchemy.ext.mutable import MutableDict
44
- from sqlalchemy.orm import backref, relationship
42
+ from sqlalchemy.orm import Mapped, backref, mapped_column, relationship
45
43
  from sqlalchemy.types import Boolean, DateTime, Integer, String, Unicode
46
44
 
45
+ from c2cgeoportal_commons.lib.literal import Literal
47
46
  from c2cgeoportal_commons.models import Base, _
48
- from c2cgeoportal_commons.models.main import Role
47
+ from c2cgeoportal_commons.models.main import AbstractLog, Role
49
48
 
50
49
  try:
51
50
  from c2cgeoform.ext.deform_ext import RelationSelect2Widget
52
51
  from colander import Email, drop
53
52
  from deform.widget import DateTimeInputWidget, HiddenWidget
54
53
  except ModuleNotFoundError:
55
- drop = None
54
+ drop = None # pylint: disable=invalid-name
56
55
 
57
56
  class GenericClass:
57
+ """Generic class."""
58
+
58
59
  def __init__(self, *args: Any, **kwargs: Any):
59
60
  pass
60
61
 
@@ -62,10 +63,11 @@ except ModuleNotFoundError:
62
63
  HiddenWidget = GenericClass
63
64
  DateTimeInputWidget = GenericClass
64
65
  CollenderGeometry = GenericClass
65
- RelationSelect2Widget = GenericClass
66
+ RelationSelect2Widget = GenericClass # type: ignore[misc,assignment]
66
67
 
67
68
 
68
- LOG = logging.getLogger(__name__)
69
+ _LOG = logging.getLogger(__name__)
70
+ _OPENID_CONNECT_ENABLED = os.environ.get("OPENID_CONNECT_ENABLED", "false").lower() in ("true", "yes", "1")
69
71
 
70
72
  _schema: str = config["schema_static"] or "static"
71
73
 
@@ -79,38 +81,124 @@ user_role = Table(
79
81
  )
80
82
 
81
83
 
82
- class User(Base):
84
+ class User(Base): # type: ignore
85
+ """The user table representation."""
86
+
83
87
  __tablename__ = "user"
84
88
  __table_args__ = {"schema": _schema}
85
- __colanderalchemy_config__ = {"title": _("User"), "plural": _("Users")}
89
+ __colanderalchemy_config__ = {
90
+ "title": _("User"),
91
+ "plural": _("Users"),
92
+ "description": Literal(
93
+ _(
94
+ """
95
+ <div class="help-block">
96
+ <p>Each user may have from 1 to n roles, but each user has a default role from
97
+ which are taken some settings. The default role (defined through the
98
+ "Settings from role" selection) has an influence on the role extent and on some
99
+ functionalities regarding their configuration.</p>
100
+
101
+ <p>Role extents for users can only be set in one role, because the application
102
+ is currently not able to check multiple extents for one user, thus it is the
103
+ default role which defines this unique extent.</p>
104
+
105
+ <p>Any functionality specified as <b>single</b> can be defined only once per user.
106
+ Hence, these functionalities have to be defined in the default role.</p>
107
+
108
+ <p>By default, functionalities are not specified as <b>single</b>. Currently, the
109
+ following functionalities are of <b>single</b> type:</p>
110
+
111
+ <ul>
112
+ <li><code>default_basemap</code></li>
113
+ <li><code>default_theme</code></li>
114
+ <li><code>preset_layer_filter</code></li>
115
+ <li><code>open_panel</code></li>
116
+ </ul>
117
+
118
+ <p>Any other functionality (with <b>single</b> not set or set to <code>false</code>) can
119
+ be defined in any role linked to the user.</p>
120
+ <hr>
121
+ </div>
122
+ """
123
+ )
124
+ ),
125
+ }
86
126
  __c2cgeoform_config__ = {"duplicate": True}
87
- item_type = Column(
127
+ item_type: Mapped[str] = mapped_column(
88
128
  "type", String(10), nullable=False, info={"colanderalchemy": {"widget": HiddenWidget()}}
89
129
  )
90
130
  __mapper_args__ = {"polymorphic_on": item_type, "polymorphic_identity": "user"}
91
131
 
92
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
93
- username = Column(
94
- Unicode, unique=True, nullable=False, info={"colanderalchemy": {"title": _("Username")}}
132
+ id: Mapped[int] = mapped_column(
133
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
134
+ )
135
+ username: Mapped[str] = mapped_column(
136
+ Unicode,
137
+ unique=True,
138
+ nullable=False,
139
+ info={
140
+ "colanderalchemy": (
141
+ {
142
+ "title": _("Username"),
143
+ "description": _("Name used for authentication (must be unique)."),
144
+ }
145
+ if not _OPENID_CONNECT_ENABLED
146
+ else {"widget": HiddenWidget()}
147
+ )
148
+ },
95
149
  )
96
- _password = Column("password", Unicode, nullable=False, info={"colanderalchemy": {"exclude": True}})
97
- temp_password = Column(
150
+ display_name: Mapped[str] = mapped_column(
151
+ Unicode,
152
+ info={
153
+ "colanderalchemy": {
154
+ "title": _("Display name"),
155
+ "description": _("Name displayed in the application."),
156
+ }
157
+ },
158
+ )
159
+ _password: Mapped[str] = mapped_column(
160
+ "password", Unicode, nullable=False, info={"colanderalchemy": {"exclude": True}}
161
+ )
162
+ temp_password: Mapped[str | None] = mapped_column(
98
163
  "temp_password", Unicode, nullable=True, info={"colanderalchemy": {"exclude": True}}
99
164
  )
100
- tech_data = Column(MutableDict.as_mutable(HSTORE), info={"colanderalchemy": {"exclude": True}})
101
- email = Column(
102
- Unicode, nullable=False, info={"colanderalchemy": {"title": _("Email"), "validator": Email()}}
165
+ tech_data = mapped_column(MutableDict.as_mutable(HSTORE), info={"colanderalchemy": {"exclude": True}}) # type: ignore[arg-type]
166
+ email: Mapped[str] = mapped_column(
167
+ Unicode,
168
+ nullable=False,
169
+ index=True,
170
+ info={
171
+ "colanderalchemy": {
172
+ "title": _("Email"),
173
+ "description": _(
174
+ "Used to send emails to the user, for example in case of password recovery."
175
+ ),
176
+ "validator": Email(),
177
+ }
178
+ },
103
179
  )
104
- is_password_changed = Column(
105
- Boolean, default=False, info={"colanderalchemy": {"title": _("The user changed his password")}}
180
+ is_password_changed: Mapped[bool] = mapped_column(
181
+ Boolean,
182
+ default=False,
183
+ info={
184
+ "colanderalchemy": (
185
+ {
186
+ "title": _("The user changed his password"),
187
+ "description": _("Indicates if user has changed his password."),
188
+ }
189
+ if not _OPENID_CONNECT_ENABLED
190
+ else {"exclude": True}
191
+ )
192
+ },
106
193
  )
107
194
 
108
- settings_role_id = Column(
109
- Integer,
195
+ settings_role_id: Mapped[int] = mapped_column(
196
+ Integer(),
197
+ nullable=True,
110
198
  info={
111
199
  "colanderalchemy": {
112
200
  "title": _("Settings from role"),
113
- "description": "Only used for settings not for permissions",
201
+ "description": _("Used to get some settings for the user (not for permissions)."),
114
202
  "widget": RelationSelect2Widget(
115
203
  Role, "id", "name", order_by="name", default_value=("", _("- Select -"))
116
204
  ),
@@ -129,37 +217,86 @@ class User(Base):
129
217
  Role,
130
218
  secondary=user_role,
131
219
  secondaryjoin=Role.id == user_role.c.role_id,
132
- backref=backref("users", order_by="User.username", info={"colanderalchemy": {"exclude": True}}),
133
- info={"colanderalchemy": {"title": _("Roles"), "exclude": True}},
220
+ backref=backref(
221
+ "users",
222
+ order_by="User.username",
223
+ info={
224
+ "colanderalchemy": {
225
+ "title": _("Users"),
226
+ "description": _("Users granted with this role."),
227
+ "exclude": True,
228
+ }
229
+ },
230
+ ),
231
+ info={
232
+ "colanderalchemy": {
233
+ "title": _("Roles"),
234
+ "description": _("Roles granted to the user."),
235
+ "exclude": True,
236
+ }
237
+ },
134
238
  )
135
239
 
136
- last_login = Column(
240
+ last_login: Mapped[datetime] = mapped_column(
137
241
  DateTime(timezone=True),
242
+ nullable=True,
138
243
  info={
139
- "colanderalchemy": {
140
- "title": _("Last login"),
141
- "missing": drop,
142
- "widget": DateTimeInputWidget(readonly=True),
143
- }
244
+ "colanderalchemy": (
245
+ {
246
+ "title": _("Last login"),
247
+ "description": _("Date of the user's last login."),
248
+ "missing": drop,
249
+ "widget": DateTimeInputWidget(readonly=True),
250
+ }
251
+ if not _OPENID_CONNECT_ENABLED
252
+ else {"exclude": True}
253
+ )
144
254
  },
145
255
  )
146
256
 
147
- expire_on = Column(DateTime(timezone=True), info={"colanderalchemy": {"title": _("Expiration date")}})
257
+ expire_on: Mapped[datetime | None] = mapped_column(
258
+ DateTime(timezone=True),
259
+ info={
260
+ "colanderalchemy": (
261
+ {
262
+ "title": _("Expiration date"),
263
+ "description": _("After this date the user will not be able to login anymore."),
264
+ }
265
+ if not _OPENID_CONNECT_ENABLED
266
+ else {"exclude": True}
267
+ )
268
+ },
269
+ )
148
270
 
149
- deactivated = Column(Boolean, default=False, info={"colanderalchemy": {"title": _("Deactivated")}})
271
+ deactivated: Mapped[bool] = mapped_column(
272
+ Boolean,
273
+ default=False,
274
+ info={
275
+ "colanderalchemy": (
276
+ {
277
+ "title": _("Deactivated"),
278
+ "description": _("Deactivate a user without removing it completely."),
279
+ }
280
+ if not _OPENID_CONNECT_ENABLED
281
+ else {"exclude": True}
282
+ )
283
+ },
284
+ )
150
285
 
151
- def __init__(
286
+ def __init__( # nosec
152
287
  self,
153
288
  username: str = "",
154
289
  password: str = "",
155
290
  email: str = "",
156
291
  is_password_changed: bool = False,
157
- settings_role: Role = None,
158
- roles: List[Role] = None,
159
- expire_on: datetime = None,
292
+ settings_role: Role | None = None,
293
+ roles: list[Role] | None = None,
294
+ expire_on: datetime | None = None,
160
295
  deactivated: bool = False,
296
+ display_name: str | None = None,
161
297
  ) -> None:
162
298
  self.username = username
299
+ self.display_name = display_name or username
163
300
  self.password = password
164
301
  self.tech_data = {}
165
302
  self.email = email
@@ -172,16 +309,16 @@ class User(Base):
172
309
 
173
310
  @property
174
311
  def password(self) -> str:
175
- """returns password"""
176
- return self._password # pragma: no cover
312
+ """Get the password."""
313
+ return self._password
177
314
 
178
315
  @password.setter
179
316
  def password(self, password: str) -> None:
180
- """encrypts password on the fly."""
317
+ """Encrypt password on the fly."""
181
318
  self._password = self.__encrypt_password(password)
182
319
 
183
320
  def set_temp_password(self, password: str) -> None:
184
- """encrypts password on the fly."""
321
+ """Encrypt password on the fly."""
185
322
  self.temp_password = self.__encrypt_password(password)
186
323
 
187
324
  @staticmethod
@@ -194,8 +331,8 @@ class User(Base):
194
331
  return crypt.crypt(password, crypt.METHOD_SHA512)
195
332
 
196
333
  def validate_password(self, passwd: str) -> bool:
197
- """Check the password against existing credentials.
198
- this method _MUST_ return a boolean.
334
+ """
335
+ Check the password against existing credentials. this method _MUST_ return a boolean.
199
336
 
200
337
  @param passwd: the password that was provided by the user to
201
338
  try and authenticate. This is the clear text version that we will
@@ -214,7 +351,7 @@ class User(Base):
214
351
 
215
352
  if (
216
353
  self.temp_password is not None
217
- and self.temp_password != ""
354
+ and self.temp_password != "" # nosec
218
355
  and compare_hash(self.temp_password, crypt.crypt(passwd, self.temp_password))
219
356
  ):
220
357
  self._password = self.temp_password
@@ -224,39 +361,104 @@ class User(Base):
224
361
  return False
225
362
 
226
363
  def expired(self) -> bool:
227
- return self.expire_on is not None and self.expire_on < datetime.now(pytz.utc)
364
+ return self.expire_on is not None and self.expire_on < datetime.now(timezone.utc)
228
365
 
229
366
  def update_last_login(self) -> None:
230
- self.last_login = datetime.now(pytz.utc)
367
+ self.last_login = datetime.now(timezone.utc)
231
368
 
232
369
  def __str__(self) -> str:
233
- return self.username or "" # pragma: no cover
370
+ return self.username or ""
234
371
 
235
372
 
236
- class Shorturl(Base):
373
+ class Shorturl(Base): # type: ignore
374
+ """The shorturl table representation."""
375
+
237
376
  __tablename__ = "shorturl"
238
377
  __table_args__ = {"schema": _schema}
239
- id = Column(Integer, primary_key=True)
240
- url = Column(Unicode)
241
- ref = Column(String(20), index=True, unique=True, nullable=False)
242
- creator_email = Column(Unicode(200))
243
- creation = Column(DateTime)
244
- last_hit = Column(DateTime)
245
- nb_hits = Column(Integer)
378
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
379
+ url: Mapped[str] = mapped_column(Unicode)
380
+ ref: Mapped[str] = mapped_column(String(20), index=True, unique=True, nullable=False)
381
+ creator_email: Mapped[str | None] = mapped_column(Unicode(200), nullable=True)
382
+ creation: Mapped[datetime] = mapped_column(DateTime)
383
+ last_hit: Mapped[datetime] = mapped_column(DateTime, nullable=True)
384
+ nb_hits: Mapped[int] = mapped_column(Integer)
385
+
246
386
 
387
+ class OAuth2Client(Base): # type: ignore
388
+ """The oauth2_client table representation."""
247
389
 
248
- class OAuth2Client(Base):
249
390
  __tablename__ = "oauth2_client"
250
391
  __table_args__ = {"schema": _schema}
251
392
  __colanderalchemy_config__ = {"title": _("OAuth2 Client"), "plural": _("OAuth2 Clients")}
252
393
  __c2cgeoform_config__ = {"duplicate": True}
253
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
254
- client_id = Column(Unicode, unique=True, info={"colanderalchemy": {"title": _("Client ID")}})
255
- secret = Column(Unicode, info={"colanderalchemy": {"title": _("Secret")}})
256
- redirect_uri = Column(Unicode, info={"colanderalchemy": {"title": _("Redirect URI")}})
394
+ id: Mapped[int] = mapped_column(
395
+ Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}}
396
+ )
397
+ client_id: Mapped[str] = mapped_column(
398
+ Unicode,
399
+ unique=True,
400
+ info={
401
+ "colanderalchemy": {
402
+ "title": _("Client ID"),
403
+ "description": _("The client identifier as e.-g. 'qgis'."),
404
+ }
405
+ },
406
+ )
407
+ secret: Mapped[str] = mapped_column(
408
+ Unicode,
409
+ info={
410
+ "colanderalchemy": {
411
+ "title": _("Secret"),
412
+ "description": _("The secret."),
413
+ }
414
+ },
415
+ )
416
+ redirect_uri: Mapped[str] = mapped_column(
417
+ Unicode,
418
+ info={
419
+ "colanderalchemy": {
420
+ "title": _("Redirect URI"),
421
+ "description": _(
422
+ """
423
+ URI where user should be redirected after authentication
424
+ as e.-g. 'http://127.0.0.1:7070/' in case of QGIS desktop.
425
+ """
426
+ ),
427
+ }
428
+ },
429
+ )
430
+ state_required: Mapped[bool] = mapped_column(
431
+ Boolean,
432
+ default=False,
433
+ info={
434
+ "colanderalchemy": {
435
+ "title": _("State required"),
436
+ "description": _(
437
+ "The state is required for this client (see: "
438
+ "https://auth0.com/docs/secure/attack-protection/state-parameters)."
439
+ ),
440
+ }
441
+ },
442
+ )
443
+ pkce_required: Mapped[bool] = mapped_column(
444
+ Boolean,
445
+ default=False,
446
+ info={
447
+ "colanderalchemy": {
448
+ "title": _("PKCE required"),
449
+ "description": _(
450
+ "PKCE is required for this client (see: "
451
+ "https://auth0.com/docs/get-started/authentication-and-authorization-flow/"
452
+ "authorization-code-flow-with-proof-key-for-code-exchange-pkce)."
453
+ ),
454
+ }
455
+ },
456
+ )
457
+
257
458
 
459
+ class OAuth2BearerToken(Base): # type: ignore
460
+ """The oauth2_bearertoken table representation."""
258
461
 
259
- class OAuth2BearerToken(Base):
260
462
  __tablename__ = "oauth2_bearertoken"
261
463
  __table_args__ = (
262
464
  sqlalchemy.schema.UniqueConstraint("client_id", "user_id"),
@@ -264,17 +466,24 @@ class OAuth2BearerToken(Base):
264
466
  "schema": _schema,
265
467
  },
266
468
  )
267
- id = Column(Integer, primary_key=True)
268
- client_id = Column(Integer, ForeignKey(_schema + ".oauth2_client.id", ondelete="CASCADE"), nullable=False)
469
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
470
+ client_id: Mapped[int] = mapped_column(
471
+ Integer, ForeignKey(_schema + ".oauth2_client.id", ondelete="CASCADE"), nullable=False
472
+ )
269
473
  client = relationship(OAuth2Client)
270
- user_id = Column(Integer, ForeignKey(_schema + ".user.id", ondelete="CASCADE"), nullable=False)
474
+ user_id: Mapped[int] = mapped_column(
475
+ Integer, ForeignKey(_schema + ".user.id", ondelete="CASCADE"), nullable=False
476
+ )
271
477
  user = relationship(User)
272
- access_token = Column(Unicode(100), unique=True)
273
- refresh_token = Column(Unicode(100), unique=True)
274
- expire_at = Column(DateTime(timezone=True)) # in one hour
478
+ access_token: Mapped[str] = mapped_column(Unicode(100), unique=True)
479
+ refresh_token: Mapped[str] = mapped_column(Unicode(100), unique=True)
480
+ expire_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) # in one hour
481
+ state = mapped_column(String, nullable=True)
482
+
275
483
 
484
+ class OAuth2AuthorizationCode(Base): # type: ignore
485
+ """The oauth2_authorizationcode table representation."""
276
486
 
277
- class OAuth2AuthorizationCode(Base):
278
487
  __tablename__ = "oauth2_authorizationcode"
279
488
  __table_args__ = (
280
489
  sqlalchemy.schema.UniqueConstraint("client_id", "user_id"),
@@ -282,11 +491,29 @@ class OAuth2AuthorizationCode(Base):
282
491
  "schema": _schema,
283
492
  },
284
493
  )
285
- id = Column(Integer, primary_key=True)
286
- client_id = Column(Integer, ForeignKey(_schema + ".oauth2_client.id", ondelete="CASCADE"), nullable=False)
494
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
495
+ client_id: Mapped[int] = mapped_column(
496
+ Integer, ForeignKey(_schema + ".oauth2_client.id", ondelete="CASCADE"), nullable=False
497
+ )
287
498
  client = relationship(OAuth2Client)
288
- user_id = Column(Integer, ForeignKey(_schema + ".user.id", ondelete="CASCADE"), nullable=False)
499
+ user_id: Mapped[int] = mapped_column(
500
+ Integer, ForeignKey(_schema + ".user.id", ondelete="CASCADE"), nullable=False
501
+ )
289
502
  user = relationship(User)
290
- redirect_uri = Column(Unicode)
291
- code = Column(Unicode(100), unique=True)
292
- expire_at = Column(DateTime(timezone=True)) # in 10 minutes
503
+ redirect_uri: Mapped[str] = mapped_column(Unicode)
504
+ code: Mapped[str] = mapped_column(Unicode(100), unique=True, nullable=True)
505
+ state: Mapped[str | None] = mapped_column(String, nullable=True)
506
+ challenge: Mapped[str] = mapped_column(String(128), nullable=True)
507
+ challenge_method: Mapped[str] = mapped_column(String(6), nullable=True)
508
+ expire_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) # in 10 minutes
509
+
510
+
511
+ class Log(AbstractLog):
512
+ """The static log table representation."""
513
+
514
+ __tablename__ = "log"
515
+ __table_args__ = {"schema": _schema}
516
+ __mapper_args__ = {
517
+ "polymorphic_identity": "static",
518
+ "concrete": True,
519
+ }
File without changes