Skip to content

Commit 11796f9

Browse files
authored
Merge pull request #357 from HackSoftware/f_expressions
JSON field increment with F expressions
2 parents 029a692 + a5a3bf9 commit 11796f9

File tree

8 files changed

+126
-1
lines changed

8 files changed

+126
-1
lines changed

requirements/local.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ boto3-stubs==1.26.81
1919

2020
flake8==6.0.0
2121
isort==5.12.0
22-
black==23.1.0
22+
black==23.3.0
2323
pre-commit==3.2.2

styleguide_example/blog_examples/f_expressions/__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.models import CharField, F, IntegerField, JSONField, Value
2+
from django.db.models.expressions import Func
3+
from django.db.models.functions import Cast
4+
5+
6+
class JSONIncrement(Func):
7+
"""
8+
This is an example from this blog post
9+
- https://www.hacksoft.io/blog/django-jsonfield-incrementation-with-f-expressions
10+
Django docs reference
11+
- https://docs.djangoproject.com/en/4.2/ref/models/expressions/#f-expressions
12+
"""
13+
14+
function = "jsonb_set"
15+
16+
def __init__(self, full_path, value=1, **extra):
17+
field_name, *key_path_parts = full_path.split("__")
18+
19+
if not field_name:
20+
raise ValueError("`full_path` can not be blank.")
21+
22+
if len(key_path_parts) < 1:
23+
raise ValueError("`full_path` must contain at least one key.")
24+
25+
key_path = ",".join(key_path_parts)
26+
27+
new_value_expr = Cast(Cast(F(full_path), IntegerField()) + value, CharField())
28+
29+
expressions = [F(field_name), Value(f"{{{key_path}}}"), Cast(new_value_expr, JSONField())]
30+
31+
super().__init__(*expressions, output_field=JSONField(), **extra)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from django.db import models
2+
3+
4+
class SomeDataModel(models.Model):
5+
"""
6+
This is an example from this blog post
7+
- https://www.hacksoft.io/blog/django-jsonfield-incrementation-with-f-expressions
8+
"""
9+
10+
name = models.CharField(
11+
max_length=255,
12+
blank=True,
13+
)
14+
stored_field = models.JSONField(
15+
blank=True,
16+
)

styleguide_example/blog_examples/f_expressions/tests/__init__.py

Whitespace-only changes.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from django.test import TestCase
2+
3+
from styleguide_example.blog_examples.f_expressions.db_functions import JSONIncrement
4+
from styleguide_example.blog_examples.models import SomeDataModel
5+
6+
7+
class JSONIncrementTests(TestCase):
8+
def setUp(self) -> None:
9+
self.first_entity_stored_field = {"first_key": 1, "second_key": 2, "third_key": 3}
10+
self.second_entity_stored_field = {"first_key": 4, "second_key": 5, "third_key": 6}
11+
self.increment_value = 10
12+
13+
def test_json_increment_in_model(self):
14+
SomeDataModel.objects.bulk_create(
15+
[
16+
SomeDataModel(name="First name", stored_field=self.first_entity_stored_field),
17+
SomeDataModel(name="Second name", stored_field=self.second_entity_stored_field),
18+
]
19+
)
20+
21+
first_entity = SomeDataModel.objects.first()
22+
second_entity = SomeDataModel.objects.last()
23+
24+
expected_first_entity_stored_field = self.first_entity_stored_field
25+
actual_first_entity_stored_field = first_entity.stored_field
26+
27+
expected_second_entity_stored_field = self.second_entity_stored_field
28+
actual_second_entity_stored_field = second_entity.stored_field
29+
30+
self.assertEqual(expected_first_entity_stored_field, actual_first_entity_stored_field)
31+
self.assertEqual(expected_second_entity_stored_field, actual_second_entity_stored_field)
32+
33+
SomeDataModel.objects.filter(name="First name").update(
34+
stored_field=JSONIncrement("stored_field__first_key", self.increment_value)
35+
)
36+
37+
first_entity = SomeDataModel.objects.first()
38+
second_entity = SomeDataModel.objects.last()
39+
40+
"""
41+
Expect only first_entity JSON field with name first_key to be incremented
42+
"""
43+
44+
expected_first_entity_stored_field = {
45+
"first_key": self.first_entity_stored_field["first_key"] + self.increment_value,
46+
"second_key": self.first_entity_stored_field["second_key"],
47+
"third_key": self.first_entity_stored_field["third_key"],
48+
}
49+
actual_first_entity_stored_field = first_entity.stored_field
50+
51+
expected_second_entity_stored_field = self.second_entity_stored_field
52+
actual_second_entity_stored_field = second_entity.stored_field
53+
54+
self.assertEqual(expected_first_entity_stored_field, actual_first_entity_stored_field)
55+
self.assertEqual(expected_second_entity_stored_field, actual_second_entity_stored_field)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 4.1.6 on 2023-05-10 06:34
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('blog_examples', '0001_initial'),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='SomeDataModel',
15+
fields=[
16+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17+
('name', models.CharField(blank=True, max_length=255)),
18+
('stored_field', models.JSONField(blank=True)),
19+
],
20+
),
21+
]

styleguide_example/blog_examples/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from django.db import models
22
from django.utils import timezone
33

4+
from .f_expressions.models import SomeDataModel # noqa
5+
46

57
class TimestampsWithAuto(models.Model):
68
created_at = models.DateTimeField(auto_now_add=True)

0 commit comments

Comments
 (0)