Skip to content

Commit b0d48ea

Browse files
authored
Merge pull request #70 from cloudblue/fix/LITE-23027
LITE-23027 `cqrs.bulk_update` now supports `previous_data`
2 parents 87ff3f1 + 7bf22e4 commit b0d48ea

File tree

4 files changed

+118
-10
lines changed

4 files changed

+118
-10
lines changed

dj_cqrs/managers.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
# Copyright © 2021 Ingram Micro Inc. All rights reserved.
1+
# Copyright © 2022 Ingram Micro Inc. All rights reserved.
22

33
import logging
44

5+
from dj_cqrs.constants import FIELDS_TRACKER_FIELD_NAME, TRACKED_FIELDS_ATTR_NAME
6+
57
from django.core.exceptions import ValidationError
68
from django.db import Error, IntegrityError, transaction
79
from django.db.models import F, Manager
@@ -25,8 +27,7 @@ def bulk_create(self, objs, **kwargs):
2527
objs = super().bulk_create(objs, **kwargs)
2628

2729
if objs:
28-
model = type(objs[0])
29-
model.call_post_bulk_create(objs, using=self.db)
30+
self.model.call_post_bulk_create(objs, using=self.db)
3031

3132
return objs
3233

@@ -36,12 +37,33 @@ def bulk_update(self, queryset, **kwargs):
3637
:param django.db.models.QuerySet queryset: Django Queryset (f.e. filter)
3738
:param kwargs: Update kwargs
3839
"""
39-
with transaction.atomic():
40+
prev_data_mapper = {}
41+
collect_prev_data = hasattr(self.model, FIELDS_TRACKER_FIELD_NAME)
42+
43+
def list_all():
44+
return list(queryset.all())
45+
46+
with transaction.atomic(savepoint=False):
47+
if collect_prev_data:
48+
objs = list_all()
49+
if not objs:
50+
return
51+
52+
for obj in objs:
53+
prev_data_mapper[obj.pk] = getattr(obj, FIELDS_TRACKER_FIELD_NAME).current()
54+
4055
current_dt = timezone.now()
4156
result = queryset.update(
4257
cqrs_revision=F('cqrs_revision') + 1, cqrs_updated=current_dt, **kwargs,
4358
)
44-
queryset.model.call_post_update(list(queryset.all()), using=queryset.db)
59+
60+
objs = list_all()
61+
if collect_prev_data:
62+
for obj in objs:
63+
setattr(obj, TRACKED_FIELDS_ATTR_NAME, prev_data_mapper.get(obj.pk))
64+
65+
queryset.model.call_post_update(objs, using=queryset.db)
66+
4567
return result
4668

4769

dj_cqrs/tracker.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
# Copyright © 2021 Ingram Micro Inc. All rights reserved.
1+
# Copyright © 2022 Ingram Micro Inc. All rights reserved.
2+
3+
from datetime import date, datetime
24

35
from dj_cqrs.constants import ALL_BASIC_FIELDS, FIELDS_TRACKER_FIELD_NAME
46

@@ -25,6 +27,14 @@ def changed(self):
2527
def changed_initial(self):
2628
return {field: None for field in self.fields if self.get_field_value(field) is not None}
2729

30+
def get_field_value(self, field):
31+
value = super().get_field_value(field)
32+
33+
if isinstance(value, (date, datetime)):
34+
value = str(value)
35+
36+
return value
37+
2838

2939
class CQRSTracker(FieldTracker):
3040

tests/test_master/test_mixin.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
# Copyright © 2021 Ingram Micro Inc. All rights reserved.
1+
# Copyright © 2022 Ingram Micro Inc. All rights reserved.
22

3+
from datetime import timedelta
34
from time import sleep
45
from uuid import uuid4
56

@@ -636,6 +637,29 @@ def test_cqrs_tracked_fields_tracking(mocker):
636637
assert tracked_data == {'cqrs_revision': 0, 'char_field': 'Value'}
637638

638639

640+
@pytest.mark.django_db(transaction=True)
641+
def test_cqrs_tracked_fields_date_and_datetime_tracking(mocker):
642+
old_dt = now()
643+
old_d = (old_dt + timedelta(days=1)).date()
644+
645+
models.BasicFieldsModel.objects.create(
646+
int_field=1,
647+
datetime_field=old_dt,
648+
date_field=old_d,
649+
)
650+
651+
publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
652+
instance = models.BasicFieldsModel.objects.first()
653+
instance.datetime_field = now()
654+
instance.date_field = now().date()
655+
instance.save()
656+
657+
tracked_data = instance.get_tracked_fields_data()
658+
assert publisher_mock.call_args[0][0].previous_data == tracked_data == {
659+
'cqrs_revision': 0, 'datetime_field': str(old_dt), 'date_field': str(old_d),
660+
}
661+
662+
639663
def test_mptt_cqrs_tracked_fields_model_has_tracker():
640664
instance = models.MPTTWithTrackingModel()
641665
tracker = getattr(instance, FIELDS_TRACKER_FIELD_NAME)

tests/test_master/test_signals.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright © 2021 Ingram Micro Inc. All rights reserved.
1+
# Copyright © 2022 Ingram Micro Inc. All rights reserved.
22

33
from datetime import datetime, timezone
44

@@ -10,7 +10,7 @@
1010
import pytest
1111

1212
from tests.dj_master import models
13-
from tests.utils import assert_publisher_once_called_with_args
13+
from tests.utils import assert_is_sub_dict, assert_publisher_once_called_with_args
1414

1515

1616
@pytest.mark.parametrize('model', (models.AllFieldsModel, models.BasicFieldsModel))
@@ -136,7 +136,7 @@ def test_automatic_post_bulk_create(mocker):
136136

137137

138138
@pytest.mark.django_db(transaction=True)
139-
def test_post_bulk_update(mocker):
139+
def test_post_bulk_update_wout_prev_data(mocker):
140140
for i in range(3):
141141
models.SimplestModel.objects.create(id=i)
142142
cqrs_updated = models.SimplestModel.objects.get(id=1).cqrs_updated
@@ -155,3 +155,55 @@ def test_post_bulk_update(mocker):
155155
m = models.SimplestModel.objects.get(id=1)
156156
assert m.cqrs_updated > cqrs_updated
157157
assert m.cqrs_revision == 1
158+
159+
160+
@pytest.mark.django_db(transaction=True)
161+
def test_post_bulk_update_with_prev_data(mocker):
162+
for i in range(3):
163+
models.SimplestTrackedModel.objects.create(id=i, description='old')
164+
165+
m = models.SimplestTrackedModel.objects.get(id=1)
166+
m.status = 'x'
167+
m.save()
168+
169+
publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
170+
models.SimplestTrackedModel.cqrs.bulk_update(
171+
queryset=models.SimplestTrackedModel.objects.filter(id__in={0, 1}).order_by('id'),
172+
description='new',
173+
status=None,
174+
)
175+
176+
m = models.SimplestTrackedModel.objects.get(id=2)
177+
assert m.cqrs_revision == 0
178+
179+
assert publisher_mock.call_count == 2
180+
for pk, prev_data in (
181+
(0, {'description': 'old', 'status': None}),
182+
(1, {'description': 'old', 'status': 'x'}),
183+
):
184+
t0_payload = publisher_mock.call_args_list[pk][0][0]
185+
assert t0_payload.signal_type == SignalType.SAVE
186+
assert t0_payload.cqrs_id == models.SimplestTrackedModel.CQRS_ID
187+
assert t0_payload.pk == pk
188+
189+
assert_is_sub_dict(
190+
{'id': pk, 'description': 'new', 'status': None},
191+
t0_payload.instance_data,
192+
)
193+
assert t0_payload.previous_data == prev_data
194+
195+
m = models.SimplestTrackedModel.objects.get(id=pk)
196+
assert m.cqrs_revision == pk + 1
197+
assert m.description == 'new'
198+
assert m.status is None
199+
200+
201+
@pytest.mark.django_db
202+
def test_post_bulk_update_nothing_to_update(mocker):
203+
publisher_mock = mocker.patch('dj_cqrs.controller.producer.produce')
204+
models.SimplestTrackedModel.cqrs.bulk_update(
205+
queryset=models.SimplestTrackedModel.objects.all(),
206+
description='something',
207+
)
208+
209+
publisher_mock.assert_not_called()

0 commit comments

Comments
 (0)