Skip to content

Commit 88a2488

Browse files
authored
Bulk create and update support for alternative managers (#709)
* Use default manager for bulk_create_with_history * Add optional manager arg to bulk_update_with_history * Update documentation
1 parent 2537104 commit 88a2488

File tree

7 files changed

+156
-9
lines changed

7 files changed

+156
-9
lines changed

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Authors
3838
- Filipe Pina (@fopina)
3939
- Florian Eßer
4040
- Frank Sachsenheim
41+
- George Kettleborough (`georgek <https://github.com/georgek>`_)
4142
- George Vilches
4243
- Gregory Bataille
4344
- Grzegorz Bialy

CHANGES.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ Unreleased
77
- Exclude ManyToManyFields when using ``bulk_create_with_history`` (gh-699)
88
- Added ``--excluded_fields`` argument to ``clean_duplicate_history`` command (gh-674)
99
- Exclude ManyToManyFields when fetching excluded fields (gh-707)
10+
- Use default model manager for ``bulk_create_with_history`` and
11+
``bulk_update_with_history`` instead of ``objects`` (gh-703)
12+
- Add optional ``manager`` argument to ``bulk_update_with_history`` to use instead of
13+
the default manager (gh-703)
1014

1115
2.11.0 (2020-06-20)
1216
-------------------

docs/common_issues.rst

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,11 @@ You can also specify a default user or default change reason responsible for the
5656
True
5757
5858
Bulk Updating a Model with History (New)
59-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
59+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
60+
6061
Bulk update was introduced with Django 2.2. We can use the utility function
61-
``bulk_update_with_history`` in order to bulk update objects using Django's ``bulk_update`` function while saving the object history:
62+
``bulk_update_with_history`` in order to bulk update objects using Django's
63+
``bulk_update`` function while saving the object history:
6264

6365

6466
.. code-block:: pycon
@@ -72,7 +74,19 @@ Bulk update was introduced with Django 2.2. We can use the utility function
7274
>>> for obj in objs: obj.question = 'Duplicate Questions'
7375
>>> bulk_update_with_history(objs, Poll, ['question'], batch_size=500)
7476
>>> Poll.objects.first().question
75-
'Duplicate Question'
77+
'Duplicate Question``
78+
79+
If your models require the use of an alternative model manager (usually because the
80+
default manager returns a filtered set), you can specify which manager to use with the
81+
``manager`` argument:
82+
83+
.. code-block:: pycon
84+
85+
>>> from simple_history.utils import bulk_update_with_history
86+
>>> from simple_history.tests.models import PollWithAlternativeManager
87+
>>>
88+
>>> data = [PollWithAlternativeManager(id=x, question='Question ' + str(x), pub_date=now()) for x in range(1000)]
89+
>>> objs = bulk_create_with_history(data, PollWithAlternativeManager, batch_size=500, manager=PollWithAlternativeManager.all_polls)
7690
7791
QuerySet Updates with History (Updated in Django 2.2)
7892
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

simple_history/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,9 @@ class RelatedNameConflictError(Exception):
1919
"""Related name conflicting with history manager"""
2020

2121
pass
22+
23+
24+
class AlternativeManagerError(Exception):
25+
"""Manager does not belong to model"""
26+
27+
pass

simple_history/tests/models.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,21 @@ class PollWithExcludedFKField(models.Model):
6363
history = HistoricalRecords(excluded_fields=["place"])
6464

6565

66+
class AlternativePollManager(models.Manager):
67+
def get_queryset(self):
68+
return super(AlternativePollManager, self).get_queryset().exclude(id=1)
69+
70+
71+
class PollWithAlternativeManager(models.Model):
72+
some_objects = AlternativePollManager()
73+
all_objects = models.Manager()
74+
75+
question = models.CharField(max_length=200)
76+
pub_date = models.DateTimeField("date published")
77+
78+
history = HistoricalRecords()
79+
80+
6681
class IPAddressHistoricalModel(models.Model):
6782
ip_address = models.GenericIPAddressField()
6883

simple_history/tests/tests/test_utils.py

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
from django.utils import timezone
99
from mock import Mock, patch
1010

11-
from simple_history.exceptions import NotHistoricalModelError
11+
from simple_history.exceptions import AlternativeManagerError, NotHistoricalModelError
1212
from simple_history.tests.models import (
1313
BulkCreateManyToManyModel,
1414
Document,
1515
Place,
1616
Poll,
17+
PollWithAlternativeManager,
1718
PollWithExcludeFields,
1819
Street,
1920
)
@@ -42,13 +43,38 @@ def setUp(self):
4243
PollWithExcludeFields(id=4, question="Question 4", pub_date=timezone.now()),
4344
PollWithExcludeFields(id=5, question="Question 5", pub_date=timezone.now()),
4445
]
46+
self.data_with_alternative_manager = [
47+
PollWithAlternativeManager(
48+
id=1, question="Question 1", pub_date=timezone.now()
49+
),
50+
PollWithAlternativeManager(
51+
id=2, question="Question 2", pub_date=timezone.now()
52+
),
53+
PollWithAlternativeManager(
54+
id=3, question="Question 3", pub_date=timezone.now()
55+
),
56+
PollWithAlternativeManager(
57+
id=4, question="Question 4", pub_date=timezone.now()
58+
),
59+
PollWithAlternativeManager(
60+
id=5, question="Question 5", pub_date=timezone.now()
61+
),
62+
]
4563

4664
def test_bulk_create_history(self):
4765
bulk_create_with_history(self.data, Poll)
4866

4967
self.assertEqual(Poll.objects.count(), 5)
5068
self.assertEqual(Poll.history.count(), 5)
5169

70+
def test_bulk_create_history_alternative_manager(self):
71+
bulk_create_with_history(
72+
self.data, PollWithAlternativeManager,
73+
)
74+
75+
self.assertEqual(PollWithAlternativeManager.all_objects.count(), 5)
76+
self.assertEqual(PollWithAlternativeManager.history.count(), 5)
77+
5278
def test_bulk_create_history_with_default_user(self):
5379
user = User.objects.create_user("tester", "[email protected]")
5480

@@ -177,7 +203,7 @@ def test_bulk_create_fails_with_wrong_model(self):
177203
def test_bulk_create_no_ids_return(self, hist_manager_mock):
178204
objects = [Place(id=1, name="Place 1")]
179205
model = Mock(
180-
objects=Mock(
206+
_default_manager=Mock(
181207
bulk_create=Mock(return_value=[Place(name="Place 1")]),
182208
filter=Mock(return_value=objects),
183209
),
@@ -311,6 +337,78 @@ def test_bulk_update_history_with_batch_size(self):
311337
self.assertEqual(Poll.history.filter(history_type="~").count(), 5)
312338

313339

340+
@skipIf(django.VERSION < (2, 2,), reason="bulk_update does not exist before 2.2")
341+
class BulkUpdateWithHistoryAlternativeManagersTestCase(TestCase):
342+
def setUp(self):
343+
self.data = [
344+
PollWithAlternativeManager(
345+
id=1, question="Question 1", pub_date=timezone.now()
346+
),
347+
PollWithAlternativeManager(
348+
id=2, question="Question 2", pub_date=timezone.now()
349+
),
350+
PollWithAlternativeManager(
351+
id=3, question="Question 3", pub_date=timezone.now()
352+
),
353+
PollWithAlternativeManager(
354+
id=4, question="Question 4", pub_date=timezone.now()
355+
),
356+
PollWithAlternativeManager(
357+
id=5, question="Question 5", pub_date=timezone.now()
358+
),
359+
]
360+
bulk_create_with_history(
361+
self.data, PollWithAlternativeManager,
362+
)
363+
364+
def test_bulk_update_history_default_manager(self):
365+
self.data[3].question = "Updated question"
366+
367+
bulk_update_with_history(
368+
self.data, PollWithAlternativeManager, fields=["question"],
369+
)
370+
371+
self.assertEqual(PollWithAlternativeManager.all_objects.count(), 5)
372+
self.assertEqual(
373+
PollWithAlternativeManager.all_objects.get(id=4).question,
374+
"Updated question",
375+
)
376+
self.assertEqual(PollWithAlternativeManager.history.count(), 10)
377+
self.assertEqual(
378+
PollWithAlternativeManager.history.filter(history_type="~").count(), 5
379+
)
380+
381+
def test_bulk_update_history_other_manager(self):
382+
# filtered by default manager
383+
self.data[0].question = "Updated question"
384+
385+
bulk_update_with_history(
386+
self.data,
387+
PollWithAlternativeManager,
388+
fields=["question"],
389+
manager=PollWithAlternativeManager.all_objects,
390+
)
391+
392+
self.assertEqual(PollWithAlternativeManager.all_objects.count(), 5)
393+
self.assertEqual(
394+
PollWithAlternativeManager.all_objects.get(id=1).question,
395+
"Updated question",
396+
)
397+
self.assertEqual(PollWithAlternativeManager.history.count(), 10)
398+
self.assertEqual(
399+
PollWithAlternativeManager.history.filter(history_type="~").count(), 5
400+
)
401+
402+
def test_bulk_update_history_wrong_manager(self):
403+
with self.assertRaises(AlternativeManagerError):
404+
bulk_update_with_history(
405+
self.data,
406+
PollWithAlternativeManager,
407+
fields=["question"],
408+
manager=Poll.objects,
409+
)
410+
411+
314412
class UpdateChangeReasonTestCase(TestCase):
315413
def test_update_change_reason_with_excluded_fields(self):
316414
poll = PollWithExcludeFields(

simple_history/utils.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django.db.models import ManyToManyField
66
from django.forms.models import model_to_dict
77

8-
from simple_history.exceptions import NotHistoricalModelError
8+
from simple_history.exceptions import AlternativeManagerError, NotHistoricalModelError
99

1010

1111
def update_change_reason(instance, reason):
@@ -78,9 +78,11 @@ def bulk_create_with_history(
7878
if isinstance(field, ManyToManyField)
7979
]
8080
history_manager = get_history_manager_for_model(model)
81+
model_manager = model._default_manager
82+
8183
second_transaction_required = True
8284
with transaction.atomic(savepoint=False):
83-
objs_with_id = model.objects.bulk_create(objs, batch_size=batch_size)
85+
objs_with_id = model_manager.bulk_create(objs, batch_size=batch_size)
8486
if objs_with_id and objs_with_id[0].pk:
8587
second_transaction_required = False
8688
history_manager.bulk_history_create(
@@ -100,7 +102,7 @@ def bulk_create_with_history(
100102
model_to_dict(obj, exclude=exclude_fields).items(),
101103
)
102104
)
103-
obj_list += model.objects.filter(**attributes)
105+
obj_list += model_manager.filter(**attributes)
104106
history_manager.bulk_history_create(
105107
obj_list,
106108
batch_size=batch_size,
@@ -120,6 +122,7 @@ def bulk_update_with_history(
120122
default_user=None,
121123
default_change_reason=None,
122124
default_date=None,
125+
manager=None,
123126
):
124127
"""
125128
Bulk update the objects specified by objs while also bulk creating
@@ -134,15 +137,21 @@ def bulk_update_with_history(
134137
in each historical record
135138
:param default_date: Optional date to specify as the history_date in each historical
136139
record
140+
:param manager: Optional model manager to use for the model instead of the default
141+
manager
137142
"""
138143
if django.VERSION < (2, 2,):
139144
raise NotImplementedError(
140145
"bulk_update_with_history is only available on "
141146
"Django versions 2.2 and later"
142147
)
143148
history_manager = get_history_manager_for_model(model)
149+
model_manager = manager or model._default_manager
150+
if model_manager.model is not model:
151+
raise AlternativeManagerError("The given manager does not belong to the model.")
152+
144153
with transaction.atomic(savepoint=False):
145-
model.objects.bulk_update(objs, fields, batch_size=batch_size)
154+
model_manager.bulk_update(objs, fields, batch_size=batch_size)
146155
history_manager.bulk_history_create(
147156
objs,
148157
batch_size=batch_size,

0 commit comments

Comments
 (0)