Skip to content

Commit 1ebf2ce

Browse files
authored
Merge pull request #171 from algolia/feat/should_index_attr
feat: support attribute/property/Field for should_index
2 parents 4da7ce2 + 1baca60 commit 1ebf2ce

File tree

5 files changed

+204
-24
lines changed

5 files changed

+204
-24
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ var/
2323
.installed.cfg
2424
*.egg
2525

26+
# IDE Files
27+
*.iml
28+
.idea/
29+
2630
# PyInstaller
2731
# Usually these files are written by a python script from a template
2832
# before PyInstaller builds the exe, so as to inject date/other infos into it.

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,18 @@ class ArticleIndex(AlgoliaIndex):
230230

231231
## Restrict indexing to a subset of your data
232232

233-
You can add constraints controlling if a record must be indexed or not. `should_index` should be a callable that returns a boolean.
233+
You can add constraints controlling if a record must be indexed or not. `should_index` should be a boolean or a callable that returns a boolean.
234234

235235
```python
236+
# with a boolean attribute
237+
class Article(models.model):
238+
name = models.CharField(max_length=64)
239+
is_indexable = True
240+
241+
class ArticleIndex(AlgoliaIndex):
242+
should_index = "is_indexable"
243+
244+
# with a callable returning a boolean
236245
class Contact(models.model):
237246
name = models.CharField(max_lenght=20)
238247
age = models.IntegerField()

src/models.py

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,34 @@ class AlgoliaIndexError(Exception):
1212
class AlgoliaIndex(object):
1313
'''An index in the Algolia backend.'''
1414

15-
# Use to specify a custom field that will be used for the objectID.
15+
# Used to specify a custom field that will be used for the objectID.
1616
# This field should be unique.
1717
custom_objectID = None
1818

19-
# Use to specify the fields that should be included in the index.
19+
# Used to specify the fields that should be included in the index.
2020
fields = ()
2121

22-
# Use to specify the geo-fields that should be used for location search.
22+
# Used to specify the geo-fields that should be used for location search.
2323
# The attribute should be a callable that returns a tuple.
2424
geo_field = None
2525

26-
# Use to specify the field that should be used for filtering by tag.
26+
# Used to specify the field that should be used for filtering by tag.
2727
tags = None
2828

29-
# Use to specify the index to target on Algolia.
29+
# Used to specify the index to target on Algolia.
3030
index_name = None
3131

32-
# Use to specify the settings of the index.
32+
# Used to specify the settings of the index.
3333
settings = {}
3434

35-
# Use to specify a callable that say if the instance should be indexed.
36-
# The attribute should be a callable that returns a boolean.
35+
# Used to specify if the instance should be indexed.
36+
# The attribute should be either:
37+
# - a callable that returns a boolean.
38+
# - a BooleanField
39+
# - a boolean property or attribute
3740
should_index = None
41+
# Name of the attribute to check on instances if should_index is not a callable
42+
_should_index_is_method = False
3843

3944
# Instance of the index from algoliasearch client
4045
__index = None
@@ -52,7 +57,7 @@ def __init__(self, model, client):
5257

5358
# Avoid error when there is only one field to index
5459
if isinstance(self.fields, str):
55-
self.fields = (self.fields, )
60+
self.fields = (self.fields,)
5661

5762
# Check fields
5863
for field in self.fields:
@@ -63,7 +68,7 @@ def __init__(self, model, client):
6368
# Check custom_objectID
6469
if self.custom_objectID:
6570
if not (hasattr(model, self.custom_objectID) or
66-
(self.custom_objectID in all_fields)):
71+
(self.custom_objectID in all_fields)):
6772
raise AlgoliaIndexError('{} is not an attribute of {}.'.format(
6873
self.custom_objectID, model))
6974

@@ -91,18 +96,20 @@ def __init__(self, model, client):
9196
raise AlgoliaIndexError('{} is not an attribute of {}.'.format(
9297
self.geo_field, model))
9398

94-
# Check should_index + get the callable
99+
# Check should_index + get the callable or attribute/field name
95100
if self.should_index:
96101
if hasattr(model, self.should_index):
97102
attr = getattr(model, self.should_index)
98-
if callable(attr):
103+
if type(attr) is not bool: # if attr is a bool, we keep attr=name to getattr on instance
99104
self.should_index = attr
100-
else:
101-
raise AlgoliaIndexError('{} should be a callable.'.format(
102-
self.should_index))
105+
if callable(self.should_index):
106+
self._should_index_is_method = True
103107
else:
104-
raise AlgoliaIndexError('{} is not an attribute of {}.'.format(
105-
self.should_index, model))
108+
try:
109+
model._meta.get_field_by_name(self.should_index)
110+
except:
111+
raise AlgoliaIndexError('{} is not an attribute nor a field of {}.'.format(
112+
self.should_index, model))
106113

107114
def __set_index(self, client):
108115
'''Get an instance of Algolia Index'''
@@ -188,9 +195,9 @@ def _build_object(self, instance):
188195
return tmp
189196

190197
def update_obj_index(self, instance):
191-
'''Update the object.'''
192-
if self.should_index:
193-
if not self.should_index(instance):
198+
"""Update the object."""
199+
if self._has_should_index():
200+
if not self._should_really_index(instance):
194201
# Should not index, but since we don't now the state of the
195202
# instance, we need to send a DELETE request to ensure that if
196203
# the instance was previously indexed, it will be removed.
@@ -201,6 +208,41 @@ def update_obj_index(self, instance):
201208
self.__index.save_object(obj)
202209
logger.debug('UPDATE %s FROM %s', obj['objectID'], self.model)
203210

211+
def _has_should_index(self):
212+
"""Return True if this AlgoliaIndex has a should_index method or attribute"""
213+
return self.should_index is not None
214+
215+
def _should_index(self, instance):
216+
"""Return True if the object should be indexed (including when self.should_index is not set)."""
217+
if self._has_should_index():
218+
return self._should_really_index(instance)
219+
else:
220+
return True
221+
222+
def _should_really_index(self, instance):
223+
"""Return True if according to should_index the object should be indexed."""
224+
if self._should_index_is_method:
225+
if hasattr(self.should_index, "__self__"):
226+
# bound method, call with instance
227+
return self.should_index(instance)
228+
else:
229+
# unbound method, simply call without arguments
230+
return self.should_index()
231+
else:
232+
# property/attribute/Field, evaluate as bool
233+
attr_type = type(self.should_index)
234+
if attr_type is str:
235+
attr_value = getattr(instance, self.should_index)
236+
elif attr_type is property:
237+
attr_value = self.should_index.__get__(instance)
238+
else:
239+
raise AlgoliaIndexError('{} should be a boolean attribute or a method that returns a boolean.'.format(
240+
self.should_index))
241+
if type(attr_value) is not bool:
242+
raise AlgoliaIndexError("%s's should_index (%s) should be a boolean" % (
243+
instance.__class__.__name__, self.should_index))
244+
return attr_value
245+
204246
def delete_obj_index(self, instance):
205247
'''Delete the object.'''
206248
objectID = self.__get_objectID(instance)

tests/models.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,36 @@ class Example(models.Model):
77
address = models.CharField(max_length=200)
88
lat = models.FloatField()
99
lng = models.FloatField()
10+
is_admin = models.BooleanField(default=False)
1011
category = []
1112
locations = []
13+
index_me = True
1214

1315
def location(self):
1416
return (self.lat, self.lng)
1517

1618
def geolocations(self):
1719
return self.locations
20+
21+
def has_name(self):
22+
return self.name is not None
23+
24+
@staticmethod
25+
def static_should_index():
26+
return True
27+
28+
@staticmethod
29+
def static_should_not_index():
30+
return False
31+
32+
@property
33+
def property_should_index(self):
34+
return True
35+
36+
@property
37+
def property_should_not_index(self):
38+
return False
39+
40+
@property
41+
def property_string(self):
42+
return "foo"

tests/test_index.py

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from django.test import TestCase
2-
from django.db import models
32

43
from django.contrib.algoliasearch import AlgoliaIndex
54
from django.contrib.algoliasearch import algolia_engine
@@ -15,7 +14,8 @@ def setUp(self):
1514
name='SuperK',
1615
address='Finland',
1716
lat=63.3,
18-
lng=-32.0)
17+
lng=-32.0,
18+
is_admin=True)
1919
self.instance.category = ['Shop', 'Grocery']
2020
self.instance.locations = [
2121
{'lat': 10.3, 'lng': -20.0},
@@ -166,4 +166,104 @@ class ExampleIndex(AlgoliaIndex):
166166
fields = ('name', 'color')
167167

168168
with self.assertRaises(AlgoliaIndexError):
169-
index = ExampleIndex(Example, self.client)
169+
ExampleIndex(Example, self.client)
170+
171+
def test_should_index_method(self):
172+
class ExampleIndex(AlgoliaIndex):
173+
fields = 'name'
174+
should_index = 'has_name'
175+
176+
index = ExampleIndex(Example, self.client)
177+
self.assertTrue(index._should_index(self.instance),
178+
"We should index an instance when should_index(instance) returns True")
179+
180+
instance_should_not = Example(name=None)
181+
self.assertFalse(index._should_index(instance_should_not),
182+
"We should not index an instance when should_index(instance) returns False")
183+
184+
def test_should_index_unbound(self):
185+
class ExampleIndex(AlgoliaIndex):
186+
fields = 'name'
187+
should_index = 'static_should_index'
188+
189+
index = ExampleIndex(Example, self.client)
190+
self.assertTrue(index._should_index(self.instance),
191+
"We should index an instance when should_index() returns True")
192+
193+
class ExampleIndex(AlgoliaIndex):
194+
fields = 'name'
195+
should_index = 'static_should_not_index'
196+
197+
index = ExampleIndex(Example, self.client)
198+
instance_should_not = Example()
199+
self.assertFalse(index._should_index(instance_should_not),
200+
"We should not index an instance when should_index() returns False")
201+
202+
def test_should_index_attr(self):
203+
class ExampleIndex(AlgoliaIndex):
204+
fields = 'name'
205+
should_index = 'index_me'
206+
207+
index = ExampleIndex(Example, self.client)
208+
self.assertTrue(index._should_index(self.instance),
209+
"We should index an instance when its should_index attr is True")
210+
211+
instance_should_not = Example()
212+
instance_should_not.index_me = False
213+
self.assertFalse(index._should_index(instance_should_not),
214+
"We should not index an instance when its should_index attr is False")
215+
216+
class ExampleIndex(AlgoliaIndex):
217+
fields = 'name'
218+
should_index = 'category'
219+
220+
index = ExampleIndex(Example, self.client)
221+
with self.assertRaises(AlgoliaIndexError, msg="We should raise when the should_index attr is not boolean"):
222+
index._should_index(self.instance)
223+
224+
def test_should_index_field(self):
225+
class ExampleIndex(AlgoliaIndex):
226+
fields = 'name'
227+
should_index = 'is_admin'
228+
229+
index = ExampleIndex(Example, self.client)
230+
self.assertTrue(index._should_index(self.instance),
231+
"We should index an instance when its should_index field is True")
232+
233+
instance_should_not = Example()
234+
instance_should_not.is_admin = False
235+
self.assertFalse(index._should_index(instance_should_not),
236+
"We should not index an instance when its should_index field is False")
237+
238+
class ExampleIndex(AlgoliaIndex):
239+
fields = 'name'
240+
should_index = 'name'
241+
242+
index = ExampleIndex(Example, self.client)
243+
with self.assertRaises(AlgoliaIndexError, msg="We should raise when the should_index field is not boolean"):
244+
index._should_index(self.instance)
245+
246+
def test_should_index_property(self):
247+
class ExampleIndex(AlgoliaIndex):
248+
fields = 'name'
249+
should_index = 'property_should_index'
250+
251+
index = ExampleIndex(Example, self.client)
252+
self.assertTrue(index._should_index(self.instance),
253+
"We should index an instance when its should_index property is True")
254+
255+
class ExampleIndex(AlgoliaIndex):
256+
fields = 'name'
257+
should_index = 'property_should_not_index'
258+
259+
index = ExampleIndex(Example, self.client)
260+
self.assertFalse(index._should_index(self.instance),
261+
"We should not index an instance when its should_index property is False")
262+
263+
class ExampleIndex(AlgoliaIndex):
264+
fields = 'name'
265+
should_index = 'property_string'
266+
267+
index = ExampleIndex(Example, self.client)
268+
with self.assertRaises(AlgoliaIndexError, msg="We should raise when the should_index property is not boolean"):
269+
index._should_index(self.instance)

0 commit comments

Comments
 (0)