c2cgeoportal-commons 2.5.0.100__py3-none-any.whl → 2.9rc44__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (92) hide show
  1. c2cgeoportal_commons/__init__.py +2 -5
  2. c2cgeoportal_commons/alembic/env.py +52 -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 +22 -25
  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 +305 -0
  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 +60 -0
  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 +16 -13
  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 +78 -0
  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 +115 -0
  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 +16 -18
  73. c2cgeoportal_commons/lib/literal.py +44 -0
  74. c2cgeoportal_commons/lib/url.py +221 -0
  75. c2cgeoportal_commons/lib/validators.py +2 -4
  76. c2cgeoportal_commons/models/__init__.py +19 -15
  77. c2cgeoportal_commons/models/main.py +1268 -266
  78. c2cgeoportal_commons/models/sqlalchemy.py +15 -19
  79. c2cgeoportal_commons/models/static.py +338 -63
  80. c2cgeoportal_commons/py.typed +0 -0
  81. c2cgeoportal_commons/testing/__init__.py +18 -13
  82. c2cgeoportal_commons/testing/initializedb.py +15 -14
  83. {c2cgeoportal_commons-2.5.0.100.dist-info → c2cgeoportal_commons-2.9rc44.dist-info}/METADATA +18 -23
  84. c2cgeoportal_commons-2.9rc44.dist-info/RECORD +89 -0
  85. {c2cgeoportal_commons-2.5.0.100.dist-info → c2cgeoportal_commons-2.9rc44.dist-info}/WHEEL +1 -1
  86. tests/conftest.py +4 -6
  87. c2cgeoportal_commons-2.5.0.100.dist-info/RECORD +0 -75
  88. tests/test_interface.py +0 -27
  89. tests/test_roles.py +0 -63
  90. tests/test_users.py +0 -71
  91. tests/test_validators.py +0 -20
  92. {c2cgeoportal_commons-2.5.0.100.dist-info → c2cgeoportal_commons-2.9rc44.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,4 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- # Copyright (c) 2012-2020, 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):
64
- """ A custom type for PostgreSQL's tsvector type. """
57
+ class TsVector(UserDefinedType[dict[str, str]]): # pylint: disable=abstract-method
58
+ """A custom type for PostgreSQL's tsvector type."""
59
+
60
+ cache_ok = True
65
61
 
66
- def get_col_spec(self) -> str: # pylint: disable=no-self-use
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-2020, 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
@@ -29,31 +27,35 @@
29
27
 
30
28
 
31
29
  import crypt
32
- from datetime import datetime
30
+ import logging
31
+ import os
32
+ from datetime import datetime, timezone
33
33
  from hashlib import sha1
34
34
  from hmac import compare_digest as compare_hash
35
- import logging
36
- from typing import Any, List
35
+ from typing import Any
37
36
 
37
+ import sqlalchemy.schema
38
38
  from c2c.template.config import config
39
- import pytz
40
39
  from sqlalchemy import Column, ForeignKey, Table
41
40
  from sqlalchemy.dialects.postgresql import HSTORE
42
41
  from sqlalchemy.ext.mutable import MutableDict
43
- from sqlalchemy.orm import backref, relationship
42
+ from sqlalchemy.orm import Mapped, backref, mapped_column, relationship
44
43
  from sqlalchemy.types import Boolean, DateTime, Integer, String, Unicode
45
44
 
45
+ from c2cgeoportal_commons.lib.literal import Literal
46
46
  from c2cgeoportal_commons.models import Base, _
47
- from c2cgeoportal_commons.models.main import Role
47
+ from c2cgeoportal_commons.models.main import AbstractLog, Role
48
48
 
49
49
  try:
50
- from colander import drop, Email
51
- from deform.widget import HiddenWidget, DateTimeInputWidget
52
50
  from c2cgeoform.ext.deform_ext import RelationSelect2Widget
51
+ from colander import Email, drop
52
+ from deform.widget import DateTimeInputWidget, HiddenWidget
53
53
  except ModuleNotFoundError:
54
- drop = None
54
+ drop = None # pylint: disable=invalid-name
55
55
 
56
56
  class GenericClass:
57
+ """Generic class."""
58
+
57
59
  def __init__(self, *args: Any, **kwargs: Any):
58
60
  pass
59
61
 
@@ -61,10 +63,11 @@ except ModuleNotFoundError:
61
63
  HiddenWidget = GenericClass
62
64
  DateTimeInputWidget = GenericClass
63
65
  CollenderGeometry = GenericClass
64
- RelationSelect2Widget = GenericClass
66
+ RelationSelect2Widget = GenericClass # type: ignore[misc,assignment]
65
67
 
66
68
 
67
- 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")
68
71
 
69
72
  _schema: str = config["schema_static"] or "static"
70
73
 
@@ -72,44 +75,130 @@ _schema: str = config["schema_static"] or "static"
72
75
  user_role = Table(
73
76
  "user_role",
74
77
  Base.metadata,
75
- Column("user_id", Integer, ForeignKey(_schema + ".user.id"), primary_key=True),
78
+ Column("user_id", Integer, ForeignKey(_schema + ".user.id", ondelete="CASCADE"), primary_key=True),
76
79
  Column("role_id", Integer, primary_key=True),
77
80
  schema=_schema,
78
81
  )
79
82
 
80
83
 
81
- class User(Base):
84
+ class User(Base): # type: ignore
85
+ """The user table representation."""
86
+
82
87
  __tablename__ = "user"
83
88
  __table_args__ = {"schema": _schema}
84
- __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
+ }
85
126
  __c2cgeoform_config__ = {"duplicate": True}
86
- item_type = Column(
127
+ item_type: Mapped[str] = mapped_column(
87
128
  "type", String(10), nullable=False, info={"colanderalchemy": {"widget": HiddenWidget()}}
88
129
  )
89
130
  __mapper_args__ = {"polymorphic_on": item_type, "polymorphic_identity": "user"}
90
131
 
91
- id = Column(Integer, primary_key=True, info={"colanderalchemy": {"widget": HiddenWidget()}})
92
- username = Column(
93
- 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
+ },
149
+ )
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}}
94
161
  )
95
- _password = Column("password", Unicode, nullable=False, info={"colanderalchemy": {"exclude": True}})
96
- temp_password = Column(
162
+ temp_password: Mapped[str | None] = mapped_column(
97
163
  "temp_password", Unicode, nullable=True, info={"colanderalchemy": {"exclude": True}}
98
164
  )
99
- tech_data = Column(MutableDict.as_mutable(HSTORE), info={"colanderalchemy": {"exclude": True}})
100
- email = Column(
101
- 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
+ },
102
179
  )
103
- is_password_changed = Column(
104
- 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
+ },
105
193
  )
106
194
 
107
- settings_role_id = Column(
108
- Integer,
195
+ settings_role_id: Mapped[int] = mapped_column(
196
+ Integer(),
197
+ nullable=True,
109
198
  info={
110
199
  "colanderalchemy": {
111
200
  "title": _("Settings from role"),
112
- "description": "Only used for settings not for permissions",
201
+ "description": _("Used to get some settings for the user (not for permissions)."),
113
202
  "widget": RelationSelect2Widget(
114
203
  Role, "id", "name", order_by="name", default_value=("", _("- Select -"))
115
204
  ),
@@ -128,37 +217,86 @@ class User(Base):
128
217
  Role,
129
218
  secondary=user_role,
130
219
  secondaryjoin=Role.id == user_role.c.role_id,
131
- backref=backref("users", info={"colanderalchemy": {"exclude": True}}),
132
- 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
+ },
133
238
  )
134
239
 
135
- last_login = Column(
240
+ last_login: Mapped[datetime] = mapped_column(
136
241
  DateTime(timezone=True),
242
+ nullable=True,
137
243
  info={
138
- "colanderalchemy": {
139
- "title": _("Last login"),
140
- "missing": drop,
141
- "widget": DateTimeInputWidget(readonly=True),
142
- }
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
+ )
143
254
  },
144
255
  )
145
256
 
146
- 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
+ )
147
270
 
148
- 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
+ )
149
285
 
150
- def __init__(
286
+ def __init__( # nosec
151
287
  self,
152
288
  username: str = "",
153
289
  password: str = "",
154
290
  email: str = "",
155
291
  is_password_changed: bool = False,
156
- settings_role: Role = None,
157
- roles: List[Role] = None,
158
- expire_on: datetime = None,
292
+ settings_role: Role | None = None,
293
+ roles: list[Role] | None = None,
294
+ expire_on: datetime | None = None,
159
295
  deactivated: bool = False,
296
+ display_name: str | None = None,
160
297
  ) -> None:
161
298
  self.username = username
299
+ self.display_name = display_name or username
162
300
  self.password = password
163
301
  self.tech_data = {}
164
302
  self.email = email
@@ -171,16 +309,16 @@ class User(Base):
171
309
 
172
310
  @property
173
311
  def password(self) -> str:
174
- """returns password"""
175
- return self._password # pragma: no cover
312
+ """Get the password."""
313
+ return self._password
176
314
 
177
315
  @password.setter
178
316
  def password(self, password: str) -> None:
179
- """encrypts password on the fly."""
317
+ """Encrypt password on the fly."""
180
318
  self._password = self.__encrypt_password(password)
181
319
 
182
320
  def set_temp_password(self, password: str) -> None:
183
- """encrypts password on the fly."""
321
+ """Encrypt password on the fly."""
184
322
  self.temp_password = self.__encrypt_password(password)
185
323
 
186
324
  @staticmethod
@@ -193,8 +331,8 @@ class User(Base):
193
331
  return crypt.crypt(password, crypt.METHOD_SHA512)
194
332
 
195
333
  def validate_password(self, passwd: str) -> bool:
196
- """Check the password against existing credentials.
197
- this method _MUST_ return a boolean.
334
+ """
335
+ Check the password against existing credentials. this method _MUST_ return a boolean.
198
336
 
199
337
  @param passwd: the password that was provided by the user to
200
338
  try and authenticate. This is the clear text version that we will
@@ -213,7 +351,7 @@ class User(Base):
213
351
 
214
352
  if (
215
353
  self.temp_password is not None
216
- and self.temp_password != ""
354
+ and self.temp_password != "" # nosec
217
355
  and compare_hash(self.temp_password, crypt.crypt(passwd, self.temp_password))
218
356
  ):
219
357
  self._password = self.temp_password
@@ -223,22 +361,159 @@ class User(Base):
223
361
  return False
224
362
 
225
363
  def expired(self) -> bool:
226
- 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)
227
365
 
228
366
  def update_last_login(self) -> None:
229
- self.last_login = datetime.now(pytz.utc)
367
+ self.last_login = datetime.now(timezone.utc)
230
368
 
231
369
  def __str__(self) -> str:
232
- return self.username or "" # pragma: no cover
370
+ return self.username or ""
371
+
233
372
 
373
+ class Shorturl(Base): # type: ignore
374
+ """The shorturl table representation."""
234
375
 
235
- class Shorturl(Base):
236
376
  __tablename__ = "shorturl"
237
377
  __table_args__ = {"schema": _schema}
238
- id = Column(Integer, primary_key=True)
239
- url = Column(Unicode)
240
- ref = Column(String(20), index=True, unique=True, nullable=False)
241
- creator_email = Column(Unicode(200))
242
- creation = Column(DateTime)
243
- last_hit = Column(DateTime)
244
- 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
+
386
+
387
+ class OAuth2Client(Base): # type: ignore
388
+ """The oauth2_client table representation."""
389
+
390
+ __tablename__ = "oauth2_client"
391
+ __table_args__ = {"schema": _schema}
392
+ __colanderalchemy_config__ = {"title": _("OAuth2 Client"), "plural": _("OAuth2 Clients")}
393
+ __c2cgeoform_config__ = {"duplicate": True}
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
+
458
+
459
+ class OAuth2BearerToken(Base): # type: ignore
460
+ """The oauth2_bearertoken table representation."""
461
+
462
+ __tablename__ = "oauth2_bearertoken"
463
+ __table_args__ = (
464
+ sqlalchemy.schema.UniqueConstraint("client_id", "user_id"),
465
+ {
466
+ "schema": _schema,
467
+ },
468
+ )
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
+ )
473
+ client = relationship(OAuth2Client)
474
+ user_id: Mapped[int] = mapped_column(
475
+ Integer, ForeignKey(_schema + ".user.id", ondelete="CASCADE"), nullable=False
476
+ )
477
+ user = relationship(User)
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
+
483
+
484
+ class OAuth2AuthorizationCode(Base): # type: ignore
485
+ """The oauth2_authorizationcode table representation."""
486
+
487
+ __tablename__ = "oauth2_authorizationcode"
488
+ __table_args__ = (
489
+ sqlalchemy.schema.UniqueConstraint("client_id", "user_id"),
490
+ {
491
+ "schema": _schema,
492
+ },
493
+ )
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
+ )
498
+ client = relationship(OAuth2Client)
499
+ user_id: Mapped[int] = mapped_column(
500
+ Integer, ForeignKey(_schema + ".user.id", ondelete="CASCADE"), nullable=False
501
+ )
502
+ user = relationship(User)
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
@@ -1,6 +1,4 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- # Copyright (c) 2018-2020, Camptocamp SA
1
+ # Copyright (c) 2018-2024, Camptocamp SA
4
2
  # All rights reserved.
5
3
 
6
4
  # Redistribution and use in source and binary forms, with or without
@@ -28,24 +26,31 @@
28
26
  # either expressed or implied, of the FreeBSD Project.
29
27
 
30
28
 
29
+ from typing import Any
30
+
31
+ import zope.sqlalchemy
31
32
  from sqlalchemy import engine_from_config
32
33
  from sqlalchemy.engine import Engine
33
34
  from sqlalchemy.orm import Session, configure_mappers, sessionmaker
34
35
  from transaction import TransactionManager
35
- import zope.sqlalchemy
36
36
 
37
37
 
38
- def get_engine(settings: dict, prefix: str = "sqlalchemy.") -> Engine:
38
+ def get_engine(settings: dict[str, Any], prefix: str = "sqlalchemy.") -> Engine:
39
+ """Get the engine."""
39
40
  return engine_from_config(settings, prefix)
40
41
 
41
42
 
42
- def get_session_factory(engine: Engine) -> sessionmaker:
43
+ def get_session_factory(engine: Engine) -> sessionmaker[Session]: # pylint: disable=unsubscriptable-object
44
+ """Get the session factory."""
43
45
  factory = sessionmaker()
44
46
  factory.configure(bind=engine)
45
47
  return factory
46
48
 
47
49
 
48
- def get_tm_session(session_factory: sessionmaker, transaction_manager: TransactionManager) -> Session:
50
+ def get_tm_session(
51
+ session_factory: sessionmaker[Session], # pylint: disable=unsubscriptable-object
52
+ transaction_manager: TransactionManager,
53
+ ) -> Session:
49
54
  """
50
55
  Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
51
56
 
@@ -64,21 +69,20 @@ def get_tm_session(session_factory: sessionmaker, transaction_manager: Transacti
64
69
  session_factory = get_session_factory(engine)
65
70
  with transaction.manager:
66
71
  dbsession = get_tm_session(session_factory, transaction.manager)
67
-
68
72
  """
69
73
  dbsession = session_factory()
74
+ assert isinstance(dbsession, Session)
70
75
  zope.sqlalchemy.register(dbsession, transaction_manager=transaction_manager)
71
76
  return dbsession
72
77
 
73
78
 
74
79
  def generate_mappers() -> None:
75
- """
76
- Initialize the model for a Pyramid app.
77
- """
80
+ """Initialize the model for a Pyramid app."""
78
81
 
79
82
  # import or define all models here to ensure they are attached to the
80
83
  # Base.metadata prior to any initialization routines
81
- import c2cgeoportal_commons.models.main # noqa: F401
84
+ import c2cgeoportal_commons.models.main # pylint: disable=unused-import,import-outside-toplevel
85
+ import c2cgeoportal_commons.models.static # pylint: disable=import-outside-toplevel
82
86
 
83
87
  # run configure_mappers after defining all of the models to ensure
84
88
  # all relationships can be setup
@@ -86,8 +90,9 @@ def generate_mappers() -> None:
86
90
 
87
91
 
88
92
  def get_session(
89
- settings: dict, transaction_manager: TransactionManager, prefix: str = "sqlalchemy."
93
+ settings: dict[str, Any], transaction_manager: TransactionManager, prefix: str = "sqlalchemy."
90
94
  ) -> Session:
95
+ """Get the session."""
91
96
  configure_mappers()
92
97
  engine = get_engine(settings, prefix)
93
98
  session_factory = get_session_factory(engine)