Skip to content

Commit 30ec13d

Browse files
authored
Merge pull request romgar#76 from romgar/add_option_to_deactivate_m2m_checks
Make m2m check optional, as a lot of extra queries are generated. It can be activated through `ENABLE_M2M_CHECK` option in `DirtyFieldMixin`. Fix romgar#73.
2 parents 5d27894 + 7a2206c commit 30ec13d

File tree

4 files changed

+50
-4
lines changed

4 files changed

+50
-4
lines changed

docs/index.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,15 @@ Checking many-to-many fields.
8888
----------------------------
8989
By default, dirty functions are not checking many-to-many fields. They are also a bit special, as a call to `.add()` method is directly
9090
saving the related object to the database, thus the instance is never dirty.
91-
You can still use ``check_m2m`` parameter, but you need to provide the values you want to test against:
91+
If you want to check these relations, you should set ``ENABLE_M2M_CHECK`` to ``True`` in your model inheriting from
92+
``DirtyFieldMixin``, use ``check_m2m`` parameter and provide the values you want to test against:
9293

9394
::
9495

96+
class TestM2MModel(DirtyFieldMixin, models.Model):
97+
ENABLE_M2M_CHECK = True
98+
m2m_field = models.ManyToManyField(AnotherModel)
99+
95100
>>> from tests.models import TestM2MModel
96101
>>> tm = TestM2MModel.objects.create()
97102
>>> tm2 = TestModel.objects.create()
@@ -109,6 +114,9 @@ You can still use ``check_m2m`` parameter, but you need to provide the values yo
109114
This can be useful when validating forms with m2m relations, where you receive some ids and want to know if your object
110115
in the database needs to be updated with these form values.
111116

117+
**WARNING**: this m2m mode will generate extra queries to get m2m relation values each time you will save your objects.
118+
It can have serious performance issues depending on your project.
119+
112120

113121
Saving dirty fields.
114122
----------------------------

src/dirtyfields/dirtyfields.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@
1212
class DirtyFieldsMixin(object):
1313
compare_function = (raw_compare, {})
1414

15+
# This mode has been introduced to handle some situations like this one:
16+
# https://github.com/romgar/django-dirtyfields/issues/73
17+
ENABLE_M2M_CHECK = False
18+
1519
def __init__(self, *args, **kwargs):
1620
super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
1721
post_save.connect(
1822
reset_state, sender=self.__class__,
1923
dispatch_uid='{name}-DirtyFieldsMixin-sweeper'.format(
2024
name=self.__class__.__name__))
21-
self._connect_m2m_relations()
25+
if self.ENABLE_M2M_CHECK:
26+
self._connect_m2m_relations()
2227
reset_state(sender=self.__class__, instance=self)
2328

2429
def _connect_m2m_relations(self):
@@ -83,6 +88,9 @@ def get_dirty_fields(self, check_relationship=False, check_m2m=None, verbose=Fal
8388
initial_dict = self._as_dict(check_relationship, include_primary_key=False)
8489
return initial_dict
8590

91+
if check_m2m is not None and not self.ENABLE_M2M_CHECK:
92+
raise ValueError("You can't check m2m fields if ENABLE_M2M_CHECK is set to False")
93+
8694
modified_fields = compare_states(self._as_dict(check_relationship),
8795
self._original_state,
8896
self.compare_function)
@@ -112,4 +120,5 @@ def reset_state(sender, instance, **kwargs):
112120
# original state should hold all possible dirty fields to avoid
113121
# getting a `KeyError` when checking if a field is dirty or not
114122
instance._original_state = instance._as_dict(check_relationship=True)
115-
instance._original_m2m_state = instance._as_dict_m2m()
123+
if instance.ENABLE_M2M_CHECK:
124+
instance._original_m2m_state = instance._as_dict_m2m()

tests/models.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ class TestCurrentDatetimeModel(DirtyFieldsMixin, models.Model):
6868

6969
class TestM2MModel(DirtyFieldsMixin, models.Model):
7070
m2m_field = models.ManyToManyField(TestModel)
71+
ENABLE_M2M_CHECK = True
72+
73+
74+
class TestM2MModelWithoutM2MModeEnabled(DirtyFieldsMixin, models.Model):
75+
m2m_field = models.ManyToManyField(TestModel)
7176

7277

7378
class TestModelWithCustomPK(DirtyFieldsMixin, models.Model):
@@ -91,3 +96,8 @@ def pre_save(instance, *args, **kwargs):
9196
instance.data_updated_on_presave = 'presave_value'
9297

9398
pre_save.connect(TestModelWithPreSaveSignal.pre_save, sender=TestModelWithPreSaveSignal)
99+
100+
101+
class TestModelWithoutM2MCheck(DirtyFieldsMixin, models.Model):
102+
characters = models.CharField(blank=True, max_length=80)
103+
ENABLE_M2M_CHECK = False

tests/test_m2m_fields.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22

3-
from .models import TestModel, TestM2MModel, TestModelWithCustomPK, TestM2MModelWithCustomPKOnM2M
3+
from .models import TestModel, TestM2MModel, TestModelWithCustomPK, TestM2MModelWithCustomPKOnM2M, \
4+
TestModelWithoutM2MCheck, TestM2MModelWithoutM2MModeEnabled
45

56

67
@pytest.mark.django_db
@@ -22,6 +23,16 @@ def test_dirty_fields_on_m2m():
2223
assert tm.get_dirty_fields(check_m2m={'m2m_field': set([0, tm2.id])}) == {'m2m_field': set([tm2.id])}
2324

2425

26+
@pytest.mark.django_db
27+
def test_dirty_fields_on_m2m_not_possible_if_not_enabled():
28+
tm = TestM2MModelWithoutM2MModeEnabled.objects.create()
29+
tm2 = TestModel.objects.create()
30+
tm.m2m_field.add(tm2)
31+
32+
with pytest.raises(ValueError):
33+
assert tm.get_dirty_fields(check_m2m={'m2m_field': set([tm2.id])}) == {}
34+
35+
2536
@pytest.mark.django_db
2637
def test_m2m_check_with_custom_primary_key():
2738
# test for bug: https://github.com/romgar/django-dirtyfields/issues/74
@@ -32,3 +43,11 @@ def test_m2m_check_with_custom_primary_key():
3243
# This line was triggering this error:
3344
# AttributeError: 'TestModelWithCustomPK' object has no attribute 'id'
3445
m2m_model.m2m_field.add(tm)
46+
47+
48+
@pytest.mark.django_db
49+
def test_m2m_disabled_does_not_allow_to_check_m2m_fields():
50+
tm = TestModelWithoutM2MCheck.objects.create()
51+
52+
with pytest.raises(Exception):
53+
assert tm.get_dirty_fields(check_m2m={'dummy': True})

0 commit comments

Comments
 (0)