Skip to content
This repository was archived by the owner on Aug 19, 2025. It is now read-only.

Commit d4b508a

Browse files
Add support for auto_now and auto_now_add (#140)
Co-authored-by: Amin Alaee <[email protected]>
1 parent a364e43 commit d4b508a

File tree

4 files changed

+73
-12
lines changed

4 files changed

+73
-12
lines changed

docs/declaring_models.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,23 @@ All fields are required unless one of the following is set:
5252
* `allow_blank` - A boolean. Determine if empty strings are allowed. Sets the default to `""`.
5353
* `default` - A value or a callable (function).
5454

55+
Special keyword arguments for `DateTime` and `Date` fields:
56+
57+
* `auto_now` - Automatically set the field to now every time the object is saved. Useful for “last-modified” timestamps.
58+
* `auto_now_add` - Automatically set the field to now when the object is first created. Useful for creation of timestamps.
59+
60+
Default=`datetime.date.today()` for `DateField` and `datetime.datetime.now()` for `DateTimeField`.
61+
62+
!!! note
63+
Setting `auto_now` or `auto_now_add` to True will cause the field to be read_only.
64+
5565
The following column types are supported.
5666
See `TypeSystem` for [type-specific validation keyword arguments][typesystem-fields].
5767

5868
* `orm.BigInteger()`
5969
* `orm.Boolean()`
60-
* `orm.Date()`
61-
* `orm.DateTime()`
70+
* `orm.Date(auto_now, auto_now_add)`
71+
* `orm.DateTime(auto_now, auto_now_add)`
6272
* `orm.Decimal()`
6373
* `orm.Email(max_length)`
6474
* `orm.Enum()`

orm/fields.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import typing
2+
from datetime import date, datetime
23

34
import sqlalchemy
45
import typesystem
@@ -100,16 +101,31 @@ def get_column_type(self):
100101
return sqlalchemy.Boolean()
101102

102103

103-
class DateTime(ModelField):
104+
class AutoNowMixin(ModelField):
105+
def __init__(self, auto_now=False, auto_now_add=False, **kwargs):
106+
self.auto_now = auto_now
107+
self.auto_now_add = auto_now_add
108+
if auto_now_add and auto_now:
109+
raise ValueError("auto_now and auto_now_add cannot be both True")
110+
if auto_now_add or auto_now:
111+
kwargs["read_only"] = True
112+
super().__init__(**kwargs)
113+
114+
115+
class DateTime(AutoNowMixin):
104116
def get_validator(self, **kwargs) -> typesystem.Field:
117+
if self.auto_now_add or self.auto_now:
118+
kwargs["default"] = datetime.now
105119
return typesystem.DateTime(**kwargs)
106120

107121
def get_column_type(self):
108122
return sqlalchemy.DateTime()
109123

110124

111-
class Date(ModelField):
125+
class Date(AutoNowMixin):
112126
def get_validator(self, **kwargs) -> typesystem.Field:
127+
if self.auto_now_add or self.auto_now:
128+
kwargs["default"] = date.today
113129
return typesystem.Date(**kwargs)
114130

115131
def get_column_type(self):

orm/models.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from sqlalchemy.ext.asyncio import create_async_engine
77

88
from orm.exceptions import MultipleMatches, NoMatch
9-
from orm.fields import String, Text
9+
from orm.fields import Date, DateTime, String, Text
1010

1111
FILTER_OPERATORS = {
1212
"exact": "__eq__",
@@ -21,6 +21,15 @@
2121
}
2222

2323

24+
def _update_auto_now_fields(values, fields):
25+
for key, value in fields.items():
26+
if isinstance(value, DateTime) and value.auto_now:
27+
values[key] = value.validator.get_default_value()
28+
elif isinstance(value, Date) and value.auto_now:
29+
values[key] = value.validator.get_default_value()
30+
return values
31+
32+
2433
class ModelRegistry:
2534
def __init__(self, database: databases.Database) -> None:
2635
self.database = database
@@ -400,7 +409,6 @@ async def create(self, **kwargs):
400409
fields={key: value.validator for key, value in fields.items()}
401410
)
402411
kwargs = validator.validate(kwargs)
403-
404412
for key, value in fields.items():
405413
if value.validator.read_only and value.validator.has_default():
406414
kwargs[key] = value.validator.get_default_value()
@@ -429,8 +437,9 @@ async def update(self, **kwargs) -> None:
429437
if key in kwargs
430438
}
431439
validator = typesystem.Schema(fields=fields)
432-
kwargs = validator.validate(kwargs)
433-
440+
kwargs = _update_auto_now_fields(
441+
validator.validate(kwargs), self.model_cls.fields
442+
)
434443
expr = self.table.update().values(**kwargs)
435444

436445
for filter_clause in self.filter_clauses:
@@ -513,11 +522,9 @@ async def update(self, **kwargs):
513522
key: field.validator for key, field in self.fields.items() if key in kwargs
514523
}
515524
validator = typesystem.Schema(fields=fields)
516-
kwargs = validator.validate(kwargs)
517-
525+
kwargs = _update_auto_now_fields(validator.validate(kwargs), self.fields)
518526
pk_column = getattr(self.table.c, self.pkname)
519527
expr = self.table.update().values(**kwargs).where(pk_column == self.pk)
520-
521528
await self.database.execute(expr)
522529

523530
# Update the model instance.

tests/test_columns.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ class Product(orm.Model):
3333
"created": orm.DateTime(default=datetime.datetime.now),
3434
"created_day": orm.Date(default=datetime.date.today),
3535
"created_time": orm.Time(default=time),
36+
"created_date": orm.Date(auto_now_add=True),
37+
"created_datetime": orm.DateTime(auto_now_add=True),
38+
"updated_datetime": orm.DateTime(auto_now=True),
39+
"updated_date": orm.Date(auto_now=True),
3640
"data": orm.JSON(default={}),
3741
"description": orm.Text(allow_blank=True),
3842
"huge_number": orm.BigInteger(default=0),
@@ -69,10 +73,13 @@ async def rollback_transactions():
6973

7074
async def test_model_crud():
7175
product = await Product.objects.create()
72-
7376
product = await Product.objects.get(pk=product.pk)
7477
assert product.created.year == datetime.datetime.now().year
7578
assert product.created_day == datetime.date.today()
79+
assert product.created_date == datetime.date.today()
80+
assert product.created_datetime.date() == datetime.datetime.now().date()
81+
assert product.updated_date == datetime.date.today()
82+
assert product.updated_datetime.date() == datetime.datetime.now().date()
7683
assert product.data == {}
7784
assert product.description == ""
7885
assert product.huge_number == 0
@@ -96,6 +103,8 @@ async def test_model_crud():
96103
assert product.price == decimal.Decimal("999.99")
97104
assert product.uuid == uuid.UUID("01175cde-c18f-4a13-a492-21bd9e1cb01b")
98105

106+
last_updated_datetime = product.updated_datetime
107+
last_updated_date = product.updated_date
99108
user = await User.objects.create()
100109
assert isinstance(user.pk, uuid.UUID)
101110

@@ -114,3 +123,22 @@ async def test_model_crud():
114123
user = await User.objects.get()
115124
assert isinstance(user.ipaddress, (ipaddress.IPv4Address, ipaddress.IPv6Address))
116125
assert user.url == "https://encode.io"
126+
# Test auto_now update
127+
await product.update(
128+
data={"foo": 1234},
129+
)
130+
assert product.updated_datetime != last_updated_datetime
131+
assert product.updated_date == last_updated_date
132+
133+
134+
async def test_both_auto_now_and_auto_now_add_raise_error():
135+
with pytest.raises(ValueError):
136+
137+
class Product(orm.Model):
138+
registry = models
139+
fields = {
140+
"id": orm.Integer(primary_key=True),
141+
"created_datetime": orm.DateTime(auto_now_add=True, auto_now=True),
142+
}
143+
144+
await Product.objects.create()

0 commit comments

Comments
 (0)