Skip to content

Commit e4104ad

Browse files
committed
Use the model_update service for user_update
1 parent d077ba2 commit e4104ad

File tree

4 files changed

+133
-71
lines changed

4 files changed

+133
-71
lines changed

styleguide_example/common/services.py

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,50 @@
1-
from typing import TypeVar
1+
from typing import List, Dict, Any, Tuple
22

3-
from django.db import transaction
43

5-
T = TypeVar('T')
6-
7-
8-
@transaction.atomic
9-
def generic_update(*, instance: T, **fields_to_update) -> T:
4+
# TODO: Decide typing for the `instance` argument
5+
def model_update(
6+
*,
7+
instance: Any,
8+
fields: List[str],
9+
data: Dict[str, Any]
10+
) -> Tuple[Any, bool]:
1011
"""
1112
Generic update service meant to be reused in local update services
1213
1314
For example:
1415
15-
def user_update(*, user: User, **fields_to_update) -> User:
16-
user = generic_update(instance=user, **fields_to_update)
16+
def user_update(*, user: User, **data) -> User:
17+
fields = ['first_name', 'last_name']
18+
user = model_update(instance=user, fields=fields, data=data)
1719
1820
// Do other actions with the user here
1921
2022
return user
21-
"""
22-
# If the passed instance is `None` - do nothing.
23-
if instance is None:
24-
return instance
2523
26-
# If there's nothing to update - do not perform unnecessary actions.
27-
if not fields_to_update:
28-
return instance
29-
30-
for attr, value in fields_to_update.items():
31-
setattr(instance, attr, value)
32-
33-
instance.full_clean()
34-
# Update only the fields that are meant to be updated.
35-
# Django docs reference: https://docs.djangoproject.com/en/dev/ref/models/instances/#specifying-which-fields-to-save
36-
update_fields = list(fields_to_update.keys())
37-
instance.save(update_fields=update_fields)
24+
Return value: Tuple with the following elements:
25+
1. The instance we updated
26+
2. A boolean value representing whether we performed an update or not.
27+
"""
28+
has_updated = False
3829

39-
return instance
30+
if instance is None:
31+
return instance, has_updated
32+
33+
for field in fields:
34+
# Skip if a field is not present in the actual data
35+
if field not in data:
36+
continue
37+
38+
if getattr(instance, field) != data[field]:
39+
has_updated = True
40+
setattr(instance, field, data[field])
41+
42+
# Perform an update only if any of the fields was actually changed
43+
if has_updated:
44+
instance.full_clean()
45+
# Update only the fields that are meant to be updated.
46+
# Django docs reference:
47+
# https://docs.djangoproject.com/en/dev/ref/models/instances/#specifying-which-fields-to-save
48+
instance.save(update_fields=fields)
49+
50+
return instance, has_updated

styleguide_example/common/tests/services/test_generic_update.py

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import unittest
2+
from unittest.mock import Mock
3+
4+
from styleguide_example.common.services import model_update
5+
6+
7+
class ModelUpdateTests(unittest.TestCase):
8+
def setUp(self):
9+
self.instance = Mock(
10+
field_a=None,
11+
field_b=None,
12+
field_c=None
13+
)
14+
15+
def test_model_update_does_not_update_if_instance_is_none(self):
16+
updated_instance, has_updated = model_update(
17+
instance=self.instance,
18+
fields=[],
19+
data={}
20+
)
21+
22+
self.assertEqual(updated_instance, self.instance)
23+
self.assertFalse(has_updated)
24+
25+
self.assertIsNone(updated_instance.field_a)
26+
self.assertIsNone(updated_instance.field_b)
27+
self.assertIsNone(updated_instance.field_c)
28+
29+
self.instance.full_clean.assert_not_called()
30+
self.instance.save.assert_not_called()
31+
32+
def test_model_update_does_not_update_if_none_of_the_fields_are_in_the_data(self):
33+
update_fields = ['non_existing_field']
34+
data = {'field_a': 'value_a'}
35+
36+
updated_instance, has_updated = model_update(
37+
instance=self.instance,
38+
fields=update_fields,
39+
data=data
40+
)
41+
42+
self.assertEqual(updated_instance, self.instance)
43+
self.assertFalse(has_updated)
44+
45+
self.assertIsNone(updated_instance.field_a)
46+
self.assertIsNone(updated_instance.field_b)
47+
self.assertIsNone(updated_instance.field_c)
48+
49+
self.instance.full_clean.assert_not_called()
50+
self.instance.save.assert_not_called()
51+
52+
def test_model_update_updates_only_passed_fields_from_data(self):
53+
update_fields = ['field_a']
54+
data = {
55+
'field_a': 'value_a',
56+
'field_b': 'value_b'
57+
}
58+
59+
updated_instance, has_updated = model_update(
60+
instance=self.instance,
61+
fields=update_fields,
62+
data=data
63+
)
64+
65+
self.assertTrue(has_updated)
66+
67+
self.assertEqual(updated_instance.field_a, 'value_a')
68+
# Even though `field_b` is passed in `data` - it does not get updated
69+
# because it is not present in the `fields` list.
70+
self.assertIsNone(updated_instance.field_b)
71+
# `field_c` remains `None`, because it is not passed anywhere.
72+
self.assertIsNone(updated_instance.field_c)
73+
74+
self.instance.full_clean.assert_called_once()
75+
self.instance.save.assert_called_once_with(update_fields=update_fields)

styleguide_example/users/services.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
from typing import Optional
22

3+
from django.db import transaction
4+
5+
from styleguide_example.common.services import model_update
6+
37
from styleguide_example.users.models import BaseUser
48

59

@@ -18,3 +22,20 @@ def user_create(
1822
)
1923

2024
return user
25+
26+
27+
@transaction.atomic
28+
def user_update(*, user: BaseUser, **data) -> BaseUser:
29+
non_side_effect_fields = ['first_name', 'last_name']
30+
31+
user, has_updated = model_update(
32+
instance=user,
33+
fields=non_side_effect_fields,
34+
data=data
35+
)
36+
37+
# Side-effect fields update here (e.g. username is generated based on first & last name)
38+
39+
# ... some additional tasks with the user ...
40+
41+
return user

0 commit comments

Comments
 (0)