Skip to content

Commit ce9447c

Browse files
author
Clement Denoix
committed
feat(signals) Add a context decorator to disable the signals
1 parent 262164c commit ce9447c

File tree

2 files changed

+185
-0
lines changed

2 files changed

+185
-0
lines changed

algoliasearch_django/decorators.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,39 @@
1+
try:
2+
# ContextDecorator was introduced in Python 3.2
3+
from contextlib import ContextDecorator
4+
except ImportError:
5+
ContextDecorator = None
6+
from functools import WRAPPER_ASSIGNMENTS, wraps
7+
8+
from django.db.models.signals import post_save, pre_delete
9+
10+
from . import algolia_engine
11+
12+
13+
def available_attrs(fn):
14+
"""
15+
Return the list of functools-wrappable attributes on a callable.
16+
This was required as a workaround for http://bugs.python.org/issue3445
17+
under Python 2.
18+
"""
19+
return WRAPPER_ASSIGNMENTS
20+
21+
22+
if ContextDecorator is None:
23+
# ContextDecorator was introduced in Python 3.2
24+
# See https://docs.python.org/3/library/contextlib.html#contextlib.ContextDecorator
25+
class ContextDecorator:
26+
"""
27+
A base class that enables a context manager to also be used as a decorator.
28+
"""
29+
def __call__(self, func):
30+
@wraps(func, assigned=available_attrs(func))
31+
def inner(*args, **kwargs):
32+
with self:
33+
return func(*args, **kwargs)
34+
return inner
35+
36+
137
def register(model):
238
"""
339
Register the given model class and wrapped AlgoliaIndex class with the Algolia engine:
@@ -17,3 +53,44 @@ def _algolia_engine_wrapper(index_class):
1753

1854
return index_class
1955
return _algolia_engine_wrapper
56+
57+
58+
class disable_auto_indexing(ContextDecorator):
59+
"""
60+
A context decorator to disable the auto-indexing behaviour of the AlgoliaIndex
61+
62+
Can be used either as a context manager or a method decorator:
63+
>>> with disable_auto_indexing():
64+
>>> my_object.save()
65+
66+
>>> @disable_auto_indexing()
67+
>>> big_operation()
68+
"""
69+
70+
def __init__(self, model=None):
71+
if model is not None:
72+
self.models = [model]
73+
else:
74+
self.models = algolia_engine._AlgoliaEngine__registered_models
75+
76+
def __enter__(self):
77+
for model in self.models:
78+
post_save.disconnect(
79+
algolia_engine._AlgoliaEngine__post_save_receiver,
80+
sender=model
81+
)
82+
pre_delete.disconnect(
83+
algolia_engine._AlgoliaEngine__pre_delete_receiver,
84+
sender=model
85+
)
86+
87+
def __exit__(self, exc_type, exc_value, traceback):
88+
for model in self.models:
89+
post_save.connect(
90+
algolia_engine._AlgoliaEngine__post_save_receiver,
91+
sender=model
92+
)
93+
pre_delete.connect(
94+
algolia_engine._AlgoliaEngine__pre_delete_receiver,
95+
sender=model
96+
)

tests/test_decorators.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from mock import (
2+
ANY,
3+
call,
4+
patch
5+
)
6+
7+
from django.test import TestCase
8+
9+
from algoliasearch_django import algolia_engine
10+
from algoliasearch_django.decorators import disable_auto_indexing
11+
12+
from .factories import UserFactory, WebsiteFactory
13+
from .models import User
14+
15+
16+
class DecoratorsTestCase(TestCase):
17+
def test_disable_auto_indexing_as_decorator_for_all(self):
18+
"""Test that the `disable_auto_indexing` should work as a decorator for all the model"""
19+
20+
@disable_auto_indexing()
21+
def decorated_operation():
22+
WebsiteFactory()
23+
UserFactory()
24+
25+
def non_decorated_operation():
26+
WebsiteFactory()
27+
UserFactory()
28+
29+
with patch.object(algolia_engine, 'save_record') as mocked_save_record:
30+
decorated_operation()
31+
32+
# The decorated method should have prevented the indexing operations
33+
mocked_save_record.assert_not_called()
34+
35+
with patch.object(algolia_engine, 'save_record') as mocked_save_record:
36+
non_decorated_operation()
37+
38+
# The non-decorated method is not preventing the indexing operations
39+
# (the signal was correctly re-connected for both of the models)
40+
mocked_save_record.assert_has_calls([
41+
call(
42+
ANY,
43+
created=True,
44+
raw=False,
45+
sender=ANY,
46+
signal=ANY,
47+
update_fields=None,
48+
using=ANY
49+
)
50+
] * 2)
51+
52+
def test_disable_auto_indexing_as_decorator_for_model(self):
53+
"""Test that the `disable_auto_indexing` should work as a decorator for a specific model"""
54+
55+
@disable_auto_indexing(model=User)
56+
def decorated_operation():
57+
WebsiteFactory()
58+
UserFactory()
59+
60+
def non_decorated_operation():
61+
WebsiteFactory()
62+
UserFactory()
63+
64+
with patch.object(algolia_engine, 'save_record') as mocked_save_record:
65+
decorated_operation()
66+
67+
# The decorated method should have prevented the indexing operation for the `User` model
68+
# but not for the `Website` model (we get only one call)
69+
mocked_save_record.assert_called_once_with(
70+
ANY,
71+
created=True,
72+
raw=False,
73+
sender=ANY,
74+
signal=ANY,
75+
update_fields=None,
76+
using=ANY
77+
)
78+
79+
with patch.object(algolia_engine, 'save_record') as mocked_save_record:
80+
non_decorated_operation()
81+
82+
# The non-decorated method is not preventing the indexing operations
83+
# (the signal was correctly re-connected for both of the models)
84+
mocked_save_record.assert_has_calls([
85+
call(
86+
ANY,
87+
created=True,
88+
raw=False,
89+
sender=ANY,
90+
signal=ANY,
91+
update_fields=None,
92+
using=ANY
93+
)
94+
] * 2)
95+
96+
def test_disable_auto_indexing_as_context_manager(self):
97+
"""Test that the `disable_auto_indexing` should work as a context manager"""
98+
99+
with patch.object(algolia_engine, 'save_record') as mocked_save_record:
100+
with disable_auto_indexing():
101+
WebsiteFactory()
102+
103+
mocked_save_record.assert_not_called()
104+
105+
with patch.object(algolia_engine, 'save_record') as mocked_save_record:
106+
WebsiteFactory()
107+
108+
mocked_save_record.assert_called_once()

0 commit comments

Comments
 (0)