Skip to content

Commit faa514b

Browse files
authored
Merge pull request #1038 from ddabble/bug/clean-duplicate-history-exception
Fix `KeyError` when cleaning duplicate history
2 parents effbfb2 + f13f3b4 commit faa514b

File tree

6 files changed

+90
-19
lines changed

6 files changed

+90
-19
lines changed

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Authors
3838
- David Grochowski (`ThePumpingLemma <https://github.com/ThePumpingLemma>`_)
3939
- David Hite
4040
- David Smith
41+
- `ddabble <https://github.com/ddabble>`_
4142
- Dmytro Shyshov (`xahgmah <https://github.com/xahgmah>`_)
4243
- Edouard Richard (`vied12 <https://github.com/vied12>` _)
4344
- Eduardo Cuducos

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Unreleased
99
- Started using ``exists`` query instead of ``count`` in ``populate_history`` command (gh-982)
1010
- Add basic support for many-to-many fields (gh-399)
1111
- Added support for Django 4.1 (gh-1021)
12+
- Added ``tracked_fields`` attribute to historical models (gh-1038)
13+
- Fixed ``KeyError`` when running ``clean_duplicate_history`` on models with ``excluded_fields`` (gh-1038)
1214

1315
3.1.1 (2022-04-23)
1416
------------------

simple_history/manager.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,8 @@ def most_recent(self):
155155
)
156156
)
157157
tmp = []
158-
excluded_fields = getattr(self.model, "_history_excluded_fields", [])
159158

160-
for field in self.instance._meta.fields:
161-
if field.name in excluded_fields:
162-
continue
159+
for field in self.model.tracked_fields:
163160
if isinstance(field, models.ForeignKey):
164161
tmp.append(field.name + "_id")
165162
else:
@@ -263,8 +260,7 @@ def bulk_history_create(
263260
history_type=history_type,
264261
**{
265262
field.attname: getattr(instance, field.attname)
266-
for field in instance._meta.fields
267-
if field.name not in self.model._history_excluded_fields
263+
for field in self.model.tracked_fields
268264
},
269265
)
270266
if hasattr(self.model, "history_relation"):

simple_history/models.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ def _history_user_setter(historical_instance, user):
7171

7272

7373
class HistoricalRecords:
74+
DEFAULT_MODEL_NAME_PREFIX = "Historical"
75+
7476
thread = context = LocalContext() # retain thread for backwards compatibility
7577
m2m_models = {}
7678

@@ -230,7 +232,7 @@ def finalize(self, sender, **kwargs):
230232

231233
def get_history_model_name(self, model):
232234
if not self.custom_model_name:
233-
return f"Historical{model._meta.object_name}"
235+
return f"{self.DEFAULT_MODEL_NAME_PREFIX}{model._meta.object_name}"
234236
# Must be trying to use a custom history model name
235237
if callable(self.custom_model_name):
236238
name = self.custom_model_name(model._meta.object_name)
@@ -274,6 +276,7 @@ def create_history_model(self, model, inherited):
274276
"__module__": self.module,
275277
"_history_excluded_fields": self.excluded_fields,
276278
"_history_m2m_fields": self.get_m2m_fields_from_model(model),
279+
"tracked_fields": self.fields_included(model),
277280
}
278281

279282
app_module = "%s.models" % model._meta.app_label
@@ -925,9 +928,7 @@ def diff_against(self, old_history, excluded_fields=None, included_fields=None):
925928

926929
included_m2m_fields = {field.name for field in old_history._history_m2m_fields}
927930
if included_fields is None:
928-
included_fields = {
929-
f.name for f in old_history.instance_type._meta.fields if f.editable
930-
}
931+
included_fields = {f.name for f in old_history.tracked_fields if f.editable}
931932
else:
932933
included_m2m_fields = included_m2m_fields.intersection(included_fields)
933934

simple_history/tests/tests/test_commands.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,25 @@ def test_auto_cleanup_with_excluded_fields(self):
410410
)
411411
self.assertEqual(Poll.history.all().count(), 1)
412412

413+
def test_auto_cleanup_for_model_with_excluded_fields(self):
414+
p = PollWithExcludeFields.objects.create(
415+
question="Will this be deleted?", pub_date=datetime.now()
416+
)
417+
self.assertEqual(PollWithExcludeFields.history.all().count(), 1)
418+
p.pub_date = p.pub_date + timedelta(days=1)
419+
p.save()
420+
self.assertEqual(PollWithExcludeFields.history.all().count(), 2)
421+
out = StringIO()
422+
management.call_command(
423+
self.command_name, auto=True, stdout=out, stderr=StringIO()
424+
)
425+
self.assertEqual(
426+
out.getvalue(),
427+
"Removed 1 historical records for "
428+
"<class 'simple_history.tests.models.PollWithExcludeFields'>\n",
429+
)
430+
self.assertEqual(PollWithExcludeFields.history.all().count(), 1)
431+
413432

414433
class TestCleanOldHistory(TestCase):
415434
command_name = "clean_old_history"

simple_history/tests/tests/test_models.py

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
InheritedRestaurant,
7878
Library,
7979
ManyToManyModelOther,
80+
ModelWithCustomAttrOneToOneField,
8081
ModelWithExcludedManyToMany,
8182
ModelWithFkToModelWithHistoryUsingBaseModelDb,
8283
ModelWithHistoryInDifferentDb,
@@ -93,6 +94,7 @@
9394
PollChildBookWithManyToMany,
9495
PollChildRestaurantWithManyToMany,
9596
PollInfo,
97+
PollWithAlternativeManager,
9698
PollWithExcludedFieldsWithDefaults,
9799
PollWithExcludedFKField,
98100
PollWithExcludeFields,
@@ -894,33 +896,83 @@ def test_get_next_record_with_excluded_field(self):
894896

895897

896898
class CreateHistoryModelTests(unittest.TestCase):
899+
@staticmethod
900+
def create_history_model(model, inherited):
901+
custom_model_name_prefix = f"Mock{HistoricalRecords.DEFAULT_MODEL_NAME_PREFIX}"
902+
records = HistoricalRecords(
903+
# Provide a custom history model name, to prevent name collisions
904+
# with existing historical models
905+
custom_model_name=lambda name: f"{custom_model_name_prefix}{name}",
906+
)
907+
records.module = model.__module__
908+
return records.create_history_model(model, inherited)
909+
910+
def test_create_history_model_has_expected_tracked_files_attr(self):
911+
def assert_tracked_fields_equal(model, expected_field_names):
912+
from .. import models
913+
914+
history_model = getattr(
915+
models, f"{HistoricalRecords.DEFAULT_MODEL_NAME_PREFIX}{model.__name__}"
916+
)
917+
self.assertListEqual(
918+
[field.name for field in history_model.tracked_fields],
919+
expected_field_names,
920+
)
921+
922+
assert_tracked_fields_equal(
923+
Poll,
924+
["id", "question", "pub_date"],
925+
)
926+
assert_tracked_fields_equal(
927+
PollWithNonEditableField,
928+
["id", "question", "pub_date", "modified"],
929+
)
930+
assert_tracked_fields_equal(
931+
PollWithExcludeFields,
932+
["id", "question", "place"],
933+
)
934+
assert_tracked_fields_equal(
935+
PollWithExcludedFieldsWithDefaults,
936+
["id", "question"],
937+
)
938+
assert_tracked_fields_equal(
939+
PollWithExcludedFKField,
940+
["id", "question", "pub_date"],
941+
)
942+
assert_tracked_fields_equal(
943+
PollWithAlternativeManager,
944+
["id", "question", "pub_date"],
945+
)
946+
assert_tracked_fields_equal(
947+
PollWithHistoricalIPAddress,
948+
["id", "question", "pub_date"],
949+
)
950+
assert_tracked_fields_equal(
951+
ModelWithCustomAttrOneToOneField,
952+
["id", "poll"],
953+
)
954+
897955
def test_create_history_model_with_one_to_one_field_to_integer_field(self):
898-
records = HistoricalRecords()
899-
records.module = AdminProfile.__module__
900956
try:
901-
records.create_history_model(AdminProfile, False)
957+
self.create_history_model(AdminProfile, False)
902958
except Exception:
903959
self.fail(
904960
"SimpleHistory should handle foreign keys to one to one"
905961
"fields to integer fields without throwing an exception"
906962
)
907963

908964
def test_create_history_model_with_one_to_one_field_to_char_field(self):
909-
records = HistoricalRecords()
910-
records.module = Bookcase.__module__
911965
try:
912-
records.create_history_model(Bookcase, False)
966+
self.create_history_model(Bookcase, False)
913967
except Exception:
914968
self.fail(
915969
"SimpleHistory should handle foreign keys to one to one"
916970
"fields to char fields without throwing an exception."
917971
)
918972

919973
def test_create_history_model_with_multiple_one_to_ones(self):
920-
records = HistoricalRecords()
921-
records.module = MultiOneToOne.__module__
922974
try:
923-
records.create_history_model(MultiOneToOne, False)
975+
self.create_history_model(MultiOneToOne, False)
924976
except Exception:
925977
self.fail(
926978
"SimpleHistory should handle foreign keys to one to one"

0 commit comments

Comments
 (0)