Skip to content

Commit 9d232a6

Browse files
committed
Added m2m_field_name util functions
These should help with often having to check which values are returned by `m2m_field_name()` and `m2m_reverse_field_name()` - since they're both undocumented. Also, if Django changes the mentioned internal methods, it'll be easier to only update these two functions instead of all the places they are / will be used.
1 parent dc3a34e commit 9d232a6

File tree

3 files changed

+92
-4
lines changed

3 files changed

+92
-4
lines changed

simple_history/models.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -684,11 +684,8 @@ def create_historical_record_m2ms(self, history_instance, instance):
684684

685685
insert_rows = []
686686

687-
# `m2m_field_name()` is part of Django's internal API
688-
through_field_name = field.m2m_field_name()
689-
687+
through_field_name = utils.get_m2m_field_name(field)
690688
rows = through_model.objects.filter(**{through_field_name: instance})
691-
692689
for row in rows:
693690
insert_row = {"history": history_instance}
694691

simple_history/tests/tests/test_utils.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import unittest
12
from datetime import datetime
23
from unittest import skipUnless
34
from unittest.mock import Mock, patch
@@ -14,22 +15,88 @@
1415
Document,
1516
Place,
1617
Poll,
18+
PollChildBookWithManyToMany,
19+
PollChildRestaurantWithManyToMany,
1720
PollWithAlternativeManager,
1821
PollWithExcludeFields,
1922
PollWithHistoricalSessionAttr,
23+
PollWithManyToMany,
24+
PollWithManyToManyCustomHistoryID,
25+
PollWithManyToManyWithIPAddress,
26+
PollWithSelfManyToMany,
27+
PollWithSeveralManyToMany,
2028
PollWithUniqueQuestion,
2129
Street,
2230
)
2331
from simple_history.utils import (
2432
bulk_create_with_history,
2533
bulk_update_with_history,
2634
get_history_manager_for_model,
35+
get_history_model_for_model,
36+
get_m2m_field_name,
37+
get_m2m_reverse_field_name,
2738
update_change_reason,
2839
)
2940

3041
User = get_user_model()
3142

3243

44+
class GetM2MFieldNamesTestCase(unittest.TestCase):
45+
def test__get_m2m_field_name__returns_expected_value(self):
46+
def field_names(model):
47+
history_model = get_history_model_for_model(model)
48+
# Sort the fields, to prevent flaky tests
49+
fields = sorted(history_model._history_m2m_fields, key=lambda f: f.name)
50+
return [get_m2m_field_name(field) for field in fields]
51+
52+
self.assertListEqual(field_names(PollWithManyToMany), ["pollwithmanytomany"])
53+
self.assertListEqual(
54+
field_names(PollWithManyToManyCustomHistoryID),
55+
["pollwithmanytomanycustomhistoryid"],
56+
)
57+
self.assertListEqual(
58+
field_names(PollWithManyToManyWithIPAddress),
59+
["pollwithmanytomanywithipaddress"],
60+
)
61+
self.assertListEqual(
62+
field_names(PollWithSeveralManyToMany), ["pollwithseveralmanytomany"] * 3
63+
)
64+
self.assertListEqual(
65+
field_names(PollChildBookWithManyToMany),
66+
["pollchildbookwithmanytomany"] * 2,
67+
)
68+
self.assertListEqual(
69+
field_names(PollChildRestaurantWithManyToMany),
70+
["pollchildrestaurantwithmanytomany"] * 2,
71+
)
72+
self.assertListEqual(
73+
field_names(PollWithSelfManyToMany), ["from_pollwithselfmanytomany"]
74+
)
75+
76+
def test__get_m2m_reverse_field_name__returns_expected_value(self):
77+
def field_names(model):
78+
history_model = get_history_model_for_model(model)
79+
# Sort the fields, to prevent flaky tests
80+
fields = sorted(history_model._history_m2m_fields, key=lambda f: f.name)
81+
return [get_m2m_reverse_field_name(field) for field in fields]
82+
83+
self.assertListEqual(field_names(PollWithManyToMany), ["place"])
84+
self.assertListEqual(field_names(PollWithManyToManyCustomHistoryID), ["place"])
85+
self.assertListEqual(field_names(PollWithManyToManyWithIPAddress), ["place"])
86+
self.assertListEqual(
87+
field_names(PollWithSeveralManyToMany), ["book", "place", "restaurant"]
88+
)
89+
self.assertListEqual(
90+
field_names(PollChildBookWithManyToMany), ["book", "place"]
91+
)
92+
self.assertListEqual(
93+
field_names(PollChildRestaurantWithManyToMany), ["place", "restaurant"]
94+
)
95+
self.assertListEqual(
96+
field_names(PollWithSelfManyToMany), ["to_pollwithselfmanytomany"]
97+
)
98+
99+
33100
class BulkCreateWithHistoryTestCase(TestCase):
34101
def setUp(self):
35102
self.data = [

simple_history/utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,30 @@ def get_app_model_primary_key_name(model):
5757
return model._meta.pk.name
5858

5959

60+
def get_m2m_field_name(m2m_field: ManyToManyField) -> str:
61+
"""
62+
Returns the field name of an M2M field's through model that corresponds to the model
63+
the M2M field is defined on.
64+
65+
E.g. for a ``votes`` M2M field on a ``Poll`` model that references a ``Vote`` model
66+
(and with a default-generated through model), this function would return ``"poll"``.
67+
"""
68+
# This method is part of Django's internal API
69+
return m2m_field.m2m_field_name()
70+
71+
72+
def get_m2m_reverse_field_name(m2m_field: ManyToManyField) -> str:
73+
"""
74+
Returns the field name of an M2M field's through model that corresponds to the model
75+
the M2M field references.
76+
77+
E.g. for a ``votes`` M2M field on a ``Poll`` model that references a ``Vote`` model
78+
(and with a default-generated through model), this function would return ``"vote"``.
79+
"""
80+
# This method is part of Django's internal API
81+
return m2m_field.m2m_reverse_field_name()
82+
83+
6084
def bulk_create_with_history(
6185
objs,
6286
model,

0 commit comments

Comments
 (0)