forked from Github-Mirrors/canaille
feat: created
and last_modified
model attributes
This commit is contained in:
parent
62d29a00bb
commit
ffa12b0f71
10 changed files with 611 additions and 502 deletions
|
@ -9,6 +9,7 @@ Changed
|
|||
- Use default python logging configuration format. :issue:`188` :pr:`165`
|
||||
- Bump to htmx 1.99.11 :pr:`166`
|
||||
- Use the standard tomllib python module instead of `toml` starting from python 3.11 :pr:`167`
|
||||
- Add `created` and `last_modified` datetime for all models
|
||||
|
||||
[0.0.42] - 2023-12-29
|
||||
=====================
|
||||
|
|
|
@ -174,7 +174,7 @@ class LDAPObject(Model, metaclass=LDAPObjectMetaclass):
|
|||
if ldap_name == "dn":
|
||||
return self.dn_for(self.rdn_value)
|
||||
|
||||
python_single_value = "List" not in str(self.__annotations__[name])
|
||||
python_single_value = "List" not in str(self.attributes[name])
|
||||
ldap_value = self.get_ldap_attribute(ldap_name)
|
||||
return cardinalize_attribute(python_single_value, ldap_value)
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ class User(canaille.core.models.User, LDAPObject):
|
|||
|
||||
attribute_map = {
|
||||
"id": "dn",
|
||||
"created": "createTimestamp",
|
||||
"last_modified": "modifyTimestamp",
|
||||
"user_name": "uid",
|
||||
"password": "userPassword",
|
||||
"preferred_language": "preferredLanguage",
|
||||
|
@ -38,7 +40,6 @@ class User(canaille.core.models.User, LDAPObject):
|
|||
"department": "departmentNumber",
|
||||
"title": "title",
|
||||
"organization": "o",
|
||||
"last_modified": "modifyTimestamp",
|
||||
"groups": "memberOf",
|
||||
"lock_date": "pwdEndTime",
|
||||
}
|
||||
|
@ -197,6 +198,8 @@ class Group(canaille.core.models.Group, LDAPObject):
|
|||
|
||||
attribute_map = {
|
||||
"id": "dn",
|
||||
"created": "createTimestamp",
|
||||
"last_modified": "modifyTimestamp",
|
||||
"display_name": "cn",
|
||||
"members": "member",
|
||||
"description": "description",
|
||||
|
@ -246,6 +249,8 @@ class Client(canaille.oidc.models.Client, LDAPObject):
|
|||
|
||||
attribute_map = {
|
||||
"id": "dn",
|
||||
"created": "createTimestamp",
|
||||
"last_modified": "modifyTimestamp",
|
||||
"preconsent": "oauthPreconsent",
|
||||
# post_logout_redirect_uris is not yet supported by authlib
|
||||
"post_logout_redirect_uris": "oauthPostLogoutRedirectURI",
|
||||
|
@ -265,6 +270,8 @@ class AuthorizationCode(canaille.oidc.models.AuthorizationCode, LDAPObject):
|
|||
rdn_attribute = "oauthAuthorizationCodeID"
|
||||
attribute_map = {
|
||||
"id": "dn",
|
||||
"created": "createTimestamp",
|
||||
"last_modified": "modifyTimestamp",
|
||||
"authorization_code_id": "oauthAuthorizationCodeID",
|
||||
"code": "oauthCode",
|
||||
"client": "oauthClient",
|
||||
|
@ -291,6 +298,8 @@ class Token(canaille.oidc.models.Token, LDAPObject):
|
|||
rdn_attribute = "oauthTokenID"
|
||||
attribute_map = {
|
||||
"id": "dn",
|
||||
"created": "createTimestamp",
|
||||
"last_modified": "modifyTimestamp",
|
||||
"token_id": "oauthTokenID",
|
||||
"access_token": "oauthAccessToken",
|
||||
"client": "oauthClient",
|
||||
|
@ -315,6 +324,8 @@ class Consent(canaille.oidc.models.Consent, LDAPObject):
|
|||
rdn_attribute = "cn"
|
||||
attribute_map = {
|
||||
"id": "dn",
|
||||
"created": "createTimestamp",
|
||||
"last_modified": "modifyTimestamp",
|
||||
"consent_id": "cn",
|
||||
"subject": "oauthSubject",
|
||||
"client": "oauthClient",
|
||||
|
|
|
@ -85,6 +85,12 @@ class MemoryModel(Model):
|
|||
return results[0] if results else None
|
||||
|
||||
def save(self):
|
||||
self.last_modified = datetime.datetime.now(datetime.timezone.utc).replace(
|
||||
microsecond=0
|
||||
)
|
||||
if not self.created:
|
||||
self.created = self.last_modified
|
||||
|
||||
self.delete()
|
||||
|
||||
# update the id index
|
||||
|
@ -190,7 +196,7 @@ class MemoryModel(Model):
|
|||
]
|
||||
values = [value for value in values if value]
|
||||
|
||||
unique_attribute = "List" not in str(self.__annotations__[name])
|
||||
unique_attribute = "List" not in str(self.attributes[name])
|
||||
if unique_attribute:
|
||||
return values[0] if values else None
|
||||
else:
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import datetime
|
||||
from collections import ChainMap
|
||||
from typing import Optional
|
||||
|
||||
from canaille.app import classproperty
|
||||
|
||||
|
@ -6,6 +8,20 @@ from canaille.app import classproperty
|
|||
class Model:
|
||||
"""Model abstract class."""
|
||||
|
||||
created: Optional[datetime.datetime]
|
||||
"""The "DateTime" that the resource was added to the service provider.
|
||||
|
||||
This attribute MUST be a DateTime.
|
||||
"""
|
||||
|
||||
last_modified: Optional[datetime.datetime]
|
||||
"""The most recent DateTime that the details of this resource were updated
|
||||
at the service provider.
|
||||
|
||||
If this resource has never been modified since its initial creation,
|
||||
the value MUST be the same as the value of "created".
|
||||
"""
|
||||
|
||||
@classproperty
|
||||
def attributes(cls):
|
||||
return ChainMap(
|
||||
|
|
|
@ -60,7 +60,7 @@ class SqlAlchemyModel(Model):
|
|||
filter = or_(
|
||||
getattr(cls, attribute_name).ilike(f"%{query}%")
|
||||
for attribute_name in attributes
|
||||
if "str" in str(cls.__annotations__[attribute_name])
|
||||
if "str" in str(cls.attributes[attribute_name])
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -72,7 +72,7 @@ class SqlAlchemyModel(Model):
|
|||
if isinstance(value, list):
|
||||
return or_(cls.attribute_filter(name, v) for v in value)
|
||||
|
||||
multiple = "List" in str(cls.__annotations__[name])
|
||||
multiple = "List" in str(cls.attributes[name])
|
||||
if multiple:
|
||||
return getattr(cls, name).contains(value)
|
||||
|
||||
|
@ -98,6 +98,12 @@ class SqlAlchemyModel(Model):
|
|||
return getattr(self, self.identifier_attribute)
|
||||
|
||||
def save(self):
|
||||
self.last_modified = datetime.datetime.now(datetime.timezone.utc).replace(
|
||||
microsecond=0
|
||||
)
|
||||
if not self.created:
|
||||
self.created = self.last_modified
|
||||
|
||||
Backend.get().db_session.add(self)
|
||||
Backend.get().db_session.commit()
|
||||
|
||||
|
@ -124,6 +130,13 @@ class User(canaille.core.models.User, Base, SqlAlchemyModel):
|
|||
id: Mapped[str] = mapped_column(
|
||||
String, primary_key=True, default=lambda: str(uuid.uuid4())
|
||||
)
|
||||
created: Mapped[datetime.datetime] = mapped_column(
|
||||
TZDateTime(timezone=True), nullable=True
|
||||
)
|
||||
last_modified: Mapped[datetime.datetime] = mapped_column(
|
||||
TZDateTime(timezone=True), nullable=True
|
||||
)
|
||||
|
||||
user_name: Mapped[str] = mapped_column(String, unique=True, nullable=False)
|
||||
password: Mapped[str] = mapped_column(
|
||||
PasswordType(schemes=["pbkdf2_sha512"]), nullable=True
|
||||
|
@ -146,9 +159,6 @@ class User(canaille.core.models.User, Base, SqlAlchemyModel):
|
|||
department: Mapped[str] = mapped_column(String, nullable=True)
|
||||
title: Mapped[str] = mapped_column(String, nullable=True)
|
||||
organization: Mapped[str] = mapped_column(String, nullable=True)
|
||||
last_modified: Mapped[datetime.datetime] = mapped_column(
|
||||
TZDateTime(timezone=True), nullable=True
|
||||
)
|
||||
groups: Mapped[List["Group"]] = relationship(
|
||||
secondary=membership_association_table, back_populates="members"
|
||||
)
|
||||
|
@ -193,7 +203,7 @@ class User(canaille.core.models.User, Base, SqlAlchemyModel):
|
|||
return all(
|
||||
self.normalize_filter_value(attribute, value)
|
||||
in getattr(self, attribute, [])
|
||||
if "List" in str(self.__annotations__[attribute])
|
||||
if "List" in str(self.attributes[attribute])
|
||||
else self.normalize_filter_value(attribute, value)
|
||||
== getattr(self, attribute, None)
|
||||
for attribute, value in filter.items()
|
||||
|
@ -221,12 +231,6 @@ class User(canaille.core.models.User, Base, SqlAlchemyModel):
|
|||
self.password = password
|
||||
self.save()
|
||||
|
||||
def save(self):
|
||||
self.last_modified = datetime.datetime.now(datetime.timezone.utc).replace(
|
||||
microsecond=0
|
||||
)
|
||||
super().save()
|
||||
|
||||
|
||||
class Group(canaille.core.models.Group, Base, SqlAlchemyModel):
|
||||
__tablename__ = "group"
|
||||
|
@ -235,6 +239,13 @@ class Group(canaille.core.models.Group, Base, SqlAlchemyModel):
|
|||
id: Mapped[str] = mapped_column(
|
||||
String, primary_key=True, default=lambda: str(uuid.uuid4())
|
||||
)
|
||||
created: Mapped[datetime.datetime] = mapped_column(
|
||||
TZDateTime(timezone=True), nullable=True
|
||||
)
|
||||
last_modified: Mapped[datetime.datetime] = mapped_column(
|
||||
TZDateTime(timezone=True), nullable=True
|
||||
)
|
||||
|
||||
display_name: Mapped[str] = mapped_column(String)
|
||||
description: Mapped[str] = mapped_column(String, nullable=True)
|
||||
members: Mapped[List["User"]] = relationship(
|
||||
|
@ -252,11 +263,17 @@ client_audience_association_table = Table(
|
|||
|
||||
class Client(canaille.oidc.models.Client, Base, SqlAlchemyModel):
|
||||
__tablename__ = "client"
|
||||
identifier_attribute = "client_id"
|
||||
|
||||
id: Mapped[str] = mapped_column(
|
||||
String, primary_key=True, default=lambda: str(uuid.uuid4())
|
||||
)
|
||||
identifier_attribute = "client_id"
|
||||
created: Mapped[datetime.datetime] = mapped_column(
|
||||
TZDateTime(timezone=True), nullable=True
|
||||
)
|
||||
last_modified: Mapped[datetime.datetime] = mapped_column(
|
||||
TZDateTime(timezone=True), nullable=True
|
||||
)
|
||||
|
||||
description: Mapped[str] = mapped_column(String, nullable=True)
|
||||
preconsent: Mapped[bool] = mapped_column(Boolean, nullable=True)
|
||||
|
@ -301,6 +318,12 @@ class AuthorizationCode(canaille.oidc.models.AuthorizationCode, Base, SqlAlchemy
|
|||
id: Mapped[str] = mapped_column(
|
||||
String, primary_key=True, default=lambda: str(uuid.uuid4())
|
||||
)
|
||||
created: Mapped[datetime.datetime] = mapped_column(
|
||||
TZDateTime(timezone=True), nullable=True
|
||||
)
|
||||
last_modified: Mapped[datetime.datetime] = mapped_column(
|
||||
TZDateTime(timezone=True), nullable=True
|
||||
)
|
||||
|
||||
authorization_code_id: Mapped[str] = mapped_column(String, nullable=True)
|
||||
code: Mapped[str] = mapped_column(String, nullable=True)
|
||||
|
@ -338,6 +361,12 @@ class Token(canaille.oidc.models.Token, Base, SqlAlchemyModel):
|
|||
id: Mapped[str] = mapped_column(
|
||||
String, primary_key=True, default=lambda: str(uuid.uuid4())
|
||||
)
|
||||
created: Mapped[datetime.datetime] = mapped_column(
|
||||
TZDateTime(timezone=True), nullable=True
|
||||
)
|
||||
last_modified: Mapped[datetime.datetime] = mapped_column(
|
||||
TZDateTime(timezone=True), nullable=True
|
||||
)
|
||||
|
||||
token_id: Mapped[str] = mapped_column(String, nullable=True)
|
||||
access_token: Mapped[str] = mapped_column(String, nullable=True)
|
||||
|
@ -370,6 +399,12 @@ class Consent(canaille.oidc.models.Consent, Base, SqlAlchemyModel):
|
|||
id: Mapped[str] = mapped_column(
|
||||
String, primary_key=True, default=lambda: str(uuid.uuid4())
|
||||
)
|
||||
created: Mapped[datetime.datetime] = mapped_column(
|
||||
TZDateTime(timezone=True), nullable=True
|
||||
)
|
||||
last_modified: Mapped[datetime.datetime] = mapped_column(
|
||||
TZDateTime(timezone=True), nullable=True
|
||||
)
|
||||
|
||||
consent_id: Mapped[str] = mapped_column(String, nullable=True)
|
||||
subject_id: Mapped[str] = mapped_column(ForeignKey("user.id"))
|
||||
|
|
|
@ -223,14 +223,6 @@ class User:
|
|||
department: Optional[str]
|
||||
"""Identifies the name of a department."""
|
||||
|
||||
last_modified: Optional[datetime.datetime]
|
||||
"""The most recent DateTime that the details of this resource were updated
|
||||
at the service provider.
|
||||
|
||||
If this resource has never been modified since its initial creation,
|
||||
the value MUST be the same as the value of "created".
|
||||
"""
|
||||
|
||||
groups: List["Group"]
|
||||
"""A list of groups to which the user belongs, either through direct
|
||||
membership, through nested groups, or dynamically calculated.
|
||||
|
|
|
@ -39,6 +39,9 @@
|
|||
<div class="content">
|
||||
{% trans %}Account settings{% endtrans %}
|
||||
</div>
|
||||
<div class="sub header" title="{{ edited_user.created|datetimeformat }}">
|
||||
{% trans creation_datetime=edited_user.created|dateformat %}Created on {{ creation_datetime }}{% endtrans %}
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
{% call fui.render_form(form, class_="profile-form info warning") %}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,3 +1,6 @@
|
|||
import datetime
|
||||
|
||||
import freezegun
|
||||
import pytest
|
||||
|
||||
from canaille.app import models
|
||||
|
@ -236,3 +239,36 @@ def test_model_references_set_unsaved_object(
|
|||
testclient.get("/groups/foo", status=200)
|
||||
|
||||
group.delete()
|
||||
|
||||
|
||||
def test_model_creation_edition_datetime(testclient, backend):
|
||||
if "ldap" in backend.__class__.__module__:
|
||||
pytest.skip()
|
||||
|
||||
with freezegun.freeze_time("2020-01-01 02:00:00"):
|
||||
user = models.User(
|
||||
user_name="foo",
|
||||
family_name="foo",
|
||||
formatted_name="foo",
|
||||
)
|
||||
user.save()
|
||||
user.reload()
|
||||
assert user.created == datetime.datetime(
|
||||
2020, 1, 1, 2, tzinfo=datetime.timezone.utc
|
||||
)
|
||||
assert user.last_modified == datetime.datetime(
|
||||
2020, 1, 1, 2, tzinfo=datetime.timezone.utc
|
||||
)
|
||||
|
||||
with freezegun.freeze_time("2021-01-01 02:00:00"):
|
||||
user.family_name = "bar"
|
||||
user.save()
|
||||
user.reload()
|
||||
assert user.created == datetime.datetime(
|
||||
2020, 1, 1, 2, tzinfo=datetime.timezone.utc
|
||||
)
|
||||
assert user.last_modified == datetime.datetime(
|
||||
2021, 1, 1, 2, tzinfo=datetime.timezone.utc
|
||||
)
|
||||
|
||||
user.delete()
|
||||
|
|
Loading…
Reference in a new issue