Skip to content

Commit 68b2bf6

Browse files
authored
Merge pull request #94 from HackSoftware/update-service
POC: Update service
2 parents 9c602a2 + e8d9041 commit 68b2bf6

File tree

5 files changed

+134
-0
lines changed

5 files changed

+134
-0
lines changed

styleguide_example/common/services.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from typing import List, Dict, Any, Tuple
2+
3+
from styleguide_example.common.types import DjangoModelType
4+
5+
6+
def model_update(
7+
*,
8+
instance: DjangoModelType,
9+
fields: List[str],
10+
data: Dict[str, Any]
11+
) -> Tuple[DjangoModelType, bool]:
12+
"""
13+
Generic update service meant to be reused in local update services
14+
15+
For example:
16+
17+
def user_update(*, user: User, data) -> User:
18+
fields = ['first_name', 'last_name']
19+
user, has_updated = model_update(instance=user, fields=fields, data=data)
20+
21+
// Do other actions with the user here
22+
23+
return user
24+
25+
Return value: Tuple with the following elements:
26+
1. The instance we updated
27+
2. A boolean value representing whether we performed an update or not.
28+
"""
29+
has_updated = False
30+
31+
for field in fields:
32+
# Skip if a field is not present in the actual data
33+
if field not in data:
34+
continue
35+
36+
if getattr(instance, field) != data[field]:
37+
has_updated = True
38+
setattr(instance, field, data[field])
39+
40+
# Perform an update only if any of the fields was actually changed
41+
if has_updated:
42+
instance.full_clean()
43+
# Update only the fields that are meant to be updated.
44+
# Django docs reference:
45+
# https://docs.djangoproject.com/en/dev/ref/models/instances/#specifying-which-fields-to-save
46+
instance.save(update_fields=fields)
47+
48+
return instance, has_updated

styleguide_example/common/tests/services/__init__.py

Whitespace-only changes.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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_none_of_the_fields_are_in_the_data(self):
16+
update_fields = ['non_existing_field']
17+
data = {'field_a': 'value_a'}
18+
19+
updated_instance, has_updated = model_update(
20+
instance=self.instance,
21+
fields=update_fields,
22+
data=data
23+
)
24+
25+
self.assertEqual(updated_instance, self.instance)
26+
self.assertFalse(has_updated)
27+
28+
self.assertIsNone(updated_instance.field_a)
29+
self.assertIsNone(updated_instance.field_b)
30+
self.assertIsNone(updated_instance.field_c)
31+
32+
self.instance.full_clean.assert_not_called()
33+
self.instance.save.assert_not_called()
34+
35+
def test_model_update_updates_only_passed_fields_from_data(self):
36+
update_fields = ['field_a']
37+
data = {
38+
'field_a': 'value_a',
39+
'field_b': 'value_b'
40+
}
41+
42+
updated_instance, has_updated = model_update(
43+
instance=self.instance,
44+
fields=update_fields,
45+
data=data
46+
)
47+
48+
self.assertTrue(has_updated)
49+
50+
self.assertEqual(updated_instance.field_a, 'value_a')
51+
# Even though `field_b` is passed in `data` - it does not get updated
52+
# because it is not present in the `fields` list.
53+
self.assertIsNone(updated_instance.field_b)
54+
# `field_c` remains `None`, because it is not passed anywhere.
55+
self.assertIsNone(updated_instance.field_c)
56+
57+
self.instance.full_clean.assert_called_once()
58+
self.instance.save.assert_called_once_with(update_fields=update_fields)

styleguide_example/common/types.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from typing import TypeVar
2+
3+
from django.db import models
4+
5+
# Generic type for a Django model
6+
# Reference: https://mypy.readthedocs.io/en/stable/kinds_of_types.html#the-type-of-class-objects
7+
DjangoModelType = TypeVar('DjangoModelType', bound=models.Model)

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)