Skip to content

Commit 659d0c5

Browse files
authored
Merge pull request #315 from HackSoftware/blog/timestamps-autos-and-defaults
Timestamps: autos, defaults & opinions
2 parents 21fc5bf + 551b7f3 commit 659d0c5

File tree

13 files changed

+392
-3
lines changed

13 files changed

+392
-3
lines changed

config/django/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"styleguide_example.integrations.apps.IntegrationsConfig",
4242
"styleguide_example.files.apps.FilesConfig",
4343
"styleguide_example.emails.apps.EmailsConfig",
44+
"styleguide_example.blog_examples.apps.BlogExamplesConfig",
4445
]
4546

4647
THIRD_PARTY_APPS = [

styleguide_example/blog_examples/__init__.py

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class BlogExamplesConfig(AppConfig):
5+
default_auto_field = "django.db.models.BigAutoField"
6+
name = "styleguide_example.blog_examples"
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Generated by Django 4.1.3 on 2023-01-28 14:23
2+
3+
from django.db import migrations, models
4+
import django.utils.timezone
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
initial = True
10+
11+
dependencies = [
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='TimestampsOpinionated',
17+
fields=[
18+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19+
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
20+
('updated_at', models.DateTimeField(blank=True, null=True)),
21+
],
22+
),
23+
migrations.CreateModel(
24+
name='TimestampsWithAuto',
25+
fields=[
26+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
27+
('created_at', models.DateTimeField(auto_now_add=True)),
28+
('updated_at', models.DateTimeField(auto_now=True)),
29+
],
30+
),
31+
migrations.CreateModel(
32+
name='TimestampsWithAutoAndDefault',
33+
fields=[
34+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
35+
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
36+
('updated_at', models.DateTimeField(auto_now=True)),
37+
],
38+
),
39+
migrations.CreateModel(
40+
name='TimestampsWithDefault',
41+
fields=[
42+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
43+
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
44+
('updated_at', models.DateTimeField(default=django.utils.timezone.now)),
45+
],
46+
),
47+
]

styleguide_example/blog_examples/migrations/__init__.py

Whitespace-only changes.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from django.db import models
2+
from django.utils import timezone
3+
4+
5+
class TimestampsWithAuto(models.Model):
6+
created_at = models.DateTimeField(auto_now_add=True)
7+
updated_at = models.DateTimeField(auto_now=True)
8+
9+
10+
class TimestampsWithAutoAndDefault(models.Model):
11+
created_at = models.DateTimeField(default=timezone.now)
12+
updated_at = models.DateTimeField(auto_now=True)
13+
14+
15+
class TimestampsWithDefault(models.Model):
16+
created_at = models.DateTimeField(default=timezone.now)
17+
updated_at = models.DateTimeField(default=timezone.now)
18+
19+
20+
class TimestampsOpinionated(models.Model):
21+
"""
22+
We want to have the following behavior:
23+
24+
1. created_at is set by default, but can be overridden.
25+
2. updated_at is not set on initial creation (stays None).
26+
3. The service layer (check `model_update`) takes care of providing value to `updated_at`,
27+
if there's no value provided by the caller.
28+
"""
29+
30+
created_at = models.DateTimeField(default=timezone.now)
31+
updated_at = models.DateTimeField(blank=True, null=True)

styleguide_example/blog_examples/tests/__init__.py

Whitespace-only changes.

styleguide_example/blog_examples/tests/models/__init__.py

Whitespace-only changes.
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
from datetime import timedelta
2+
3+
from django.test import TestCase
4+
from django.utils import timezone
5+
6+
from styleguide_example.blog_examples.models import (
7+
TimestampsOpinionated,
8+
TimestampsWithAuto,
9+
TimestampsWithAutoAndDefault,
10+
TimestampsWithDefault,
11+
)
12+
13+
14+
class TimestampsTests(TestCase):
15+
def test_timestamps_with_auto_behavior(self):
16+
"""
17+
Timestamps are set automatically
18+
"""
19+
obj = TimestampsWithAuto()
20+
obj.full_clean()
21+
obj.save()
22+
23+
self.assertIsNotNone(obj.created_at)
24+
self.assertIsNotNone(obj.updated_at)
25+
26+
self.assertNotEqual(obj.created_at, obj.updated_at)
27+
28+
"""
29+
Timestamps cannot be overridden
30+
"""
31+
timestamp = timezone.now() - timedelta(days=1)
32+
33+
obj = TimestampsWithAuto(created_at=timestamp, updated_at=timestamp)
34+
obj.full_clean()
35+
obj.save()
36+
37+
self.assertNotEqual(timestamp, obj.created_at)
38+
self.assertNotEqual(timestamp, obj.updated_at)
39+
40+
"""
41+
updated_at gets auto updated, while created_at stays the same
42+
"""
43+
obj = TimestampsWithAuto()
44+
obj.full_clean()
45+
obj.save()
46+
47+
original_created_at = obj.created_at
48+
original_updated_at = obj.updated_at
49+
50+
obj.save()
51+
# Get a fresh object
52+
obj = TimestampsWithAuto.objects.get(id=obj.id)
53+
54+
self.assertEqual(original_created_at, obj.created_at)
55+
self.assertNotEqual(original_updated_at, obj.updated_at)
56+
57+
def test_timestamps_with_mixed_behavior(self):
58+
"""
59+
Timestamps are set automatically / by default
60+
"""
61+
obj = TimestampsWithAutoAndDefault()
62+
obj.full_clean()
63+
obj.save()
64+
65+
self.assertIsNotNone(obj.created_at)
66+
self.assertIsNotNone(obj.updated_at)
67+
68+
self.assertNotEqual(obj.created_at, obj.updated_at)
69+
70+
"""
71+
Some timestamps can be overridden
72+
"""
73+
timestamp = timezone.now() - timedelta(days=1)
74+
75+
obj = TimestampsWithAutoAndDefault(created_at=timestamp, updated_at=timestamp)
76+
obj.full_clean()
77+
obj.save()
78+
79+
# This is default
80+
self.assertEqual(timestamp, obj.created_at)
81+
# This is auto_now
82+
self.assertNotEqual(timestamp, obj.updated_at)
83+
84+
"""
85+
updated_at gets auto updated, while created_at stays the same
86+
"""
87+
obj = TimestampsWithAutoAndDefault()
88+
obj.full_clean()
89+
obj.save()
90+
91+
original_created_at = obj.created_at
92+
original_updated_at = obj.updated_at
93+
94+
obj.save()
95+
# Get a fresh object
96+
obj = TimestampsWithAutoAndDefault.objects.get(id=obj.id)
97+
98+
self.assertEqual(original_created_at, obj.created_at)
99+
self.assertNotEqual(original_updated_at, obj.updated_at)
100+
101+
def test_timestamps_with_default_behavior(self):
102+
"""
103+
Timestamps are set by default
104+
"""
105+
obj = TimestampsWithDefault()
106+
obj.full_clean()
107+
obj.save()
108+
109+
self.assertIsNotNone(obj.created_at)
110+
self.assertIsNotNone(obj.updated_at)
111+
112+
self.assertNotEqual(obj.created_at, obj.updated_at)
113+
114+
"""
115+
Both timestamps can be overridden
116+
"""
117+
timestamp = timezone.now() - timedelta(days=1)
118+
119+
obj = TimestampsWithDefault(created_at=timestamp, updated_at=timestamp)
120+
obj.full_clean()
121+
obj.save()
122+
123+
self.assertEqual(timestamp, obj.created_at)
124+
self.assertEqual(timestamp, obj.updated_at)
125+
# And by transitivity
126+
self.assertEqual(obj.created_at, obj.updated_at)
127+
128+
"""
129+
created_at / updated_at are not auto updated
130+
"""
131+
obj = TimestampsWithDefault()
132+
obj.full_clean()
133+
obj.save()
134+
135+
original_created_at = obj.created_at
136+
original_updated_at = obj.updated_at
137+
138+
obj.save()
139+
# Get a fresh object
140+
obj = TimestampsWithDefault.objects.get(id=obj.id)
141+
142+
self.assertEqual(original_created_at, obj.created_at)
143+
self.assertEqual(original_updated_at, obj.updated_at)
144+
145+
def test_timestamps_with_opinionated_behavior(self):
146+
"""
147+
created_at is only set by default
148+
"""
149+
obj = TimestampsOpinionated()
150+
obj.full_clean()
151+
obj.save()
152+
153+
self.assertIsNotNone(obj.created_at)
154+
self.assertIsNone(obj.updated_at)
155+
156+
"""
157+
Both timestamps can be overridden
158+
"""
159+
timestamp = timezone.now() - timedelta(days=1)
160+
161+
obj = TimestampsOpinionated(created_at=timestamp, updated_at=timestamp)
162+
obj.full_clean()
163+
obj.save()
164+
165+
self.assertEqual(timestamp, obj.created_at)
166+
self.assertEqual(timestamp, obj.updated_at)
167+
# And by transitivity
168+
self.assertEqual(obj.created_at, obj.updated_at)
169+
170+
"""
171+
updated_at is not auto updated, created_at stays the same
172+
"""
173+
obj = TimestampsOpinionated()
174+
obj.full_clean()
175+
obj.save()
176+
177+
original_created_at = obj.created_at
178+
original_updated_at = obj.updated_at
179+
180+
obj.save()
181+
# Get a fresh object
182+
obj = TimestampsOpinionated.objects.get(id=obj.id)
183+
184+
self.assertEqual(original_created_at, obj.created_at)
185+
self.assertEqual(original_updated_at, obj.updated_at)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 4.1.3 on 2023-01-28 14:33
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('common', '0005_simplemodel_randommodel_simple_objects'),
10+
]
11+
12+
operations = [
13+
migrations.RemoveField(
14+
model_name='simplemodel',
15+
name='created_at',
16+
),
17+
migrations.RemoveField(
18+
model_name='simplemodel',
19+
name='updated_at',
20+
),
21+
]

0 commit comments

Comments
 (0)