Skip to content

Commit d077ba2

Browse files
committed
Introduce a generic update service
1 parent 9c602a2 commit d077ba2

File tree

3 files changed

+84
-0
lines changed

3 files changed

+84
-0
lines changed

styleguide_example/common/services.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from typing import TypeVar
2+
3+
from django.db import transaction
4+
5+
T = TypeVar('T')
6+
7+
8+
@transaction.atomic
9+
def generic_update(*, instance: T, **fields_to_update) -> T:
10+
"""
11+
Generic update service meant to be reused in local update services
12+
13+
For example:
14+
15+
def user_update(*, user: User, **fields_to_update) -> User:
16+
user = generic_update(instance=user, **fields_to_update)
17+
18+
// Do other actions with the user here
19+
20+
return user
21+
"""
22+
# If the passed instance is `None` - do nothing.
23+
if instance is None:
24+
return instance
25+
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)
38+
39+
return instance

styleguide_example/common/tests/services/__init__.py

Whitespace-only changes.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import unittest
2+
from unittest.mock import Mock
3+
4+
from styleguide_example.common.services import generic_update
5+
6+
7+
class GenericUpdateTests(unittest.TestCase):
8+
def test_generic_update_returns_none_if_passed_instance_is_none(self):
9+
instance = None
10+
updated_instance = generic_update(instance=instance)
11+
12+
self.assertIsNone(updated_instance)
13+
14+
def test_generic_update_returns_none_if_no_update_fields_are_passed(self):
15+
instance = Mock()
16+
updated_instance = generic_update(instance=instance)
17+
18+
self.assertEqual(instance, updated_instance)
19+
instance.full_clean.assert_not_called()
20+
instance.save.assert_not_called()
21+
22+
def test_generic_update_sets_passed_fields_and_executes_full_clean_and_save(self):
23+
instance = Mock(
24+
field_a=None,
25+
field_b=None,
26+
field_c=None
27+
)
28+
fields_to_update = {
29+
'field_a': 'value_a',
30+
'field_b': 'value_b',
31+
}
32+
33+
self.assertIsNone(instance.field_a)
34+
self.assertIsNone(instance.field_b)
35+
self.assertIsNone(instance.field_c)
36+
37+
updated_instance = generic_update(instance=instance, **fields_to_update)
38+
39+
instance.full_clean.assert_called_once()
40+
instance.save.assert_called_once_with(update_fields=['field_a', 'field_b'])
41+
42+
self.assertEqual(updated_instance.field_a, 'value_a')
43+
self.assertEqual(updated_instance.field_b, 'value_b')
44+
# `field_c` is not updated because it is not passed as a field to update
45+
self.assertIsNone(updated_instance.field_c)

0 commit comments

Comments
 (0)