Skip to content

Commit 7a162f9

Browse files
committed
Merge pull request django-haystack#790 from andrewschoen/feature/haystack-identifier-module
Added a new setting, HAYSTACK_IDENTIFIER_METHOD, which will allow a cust...
2 parents 4004222 + 050348b commit 7a162f9

File tree

5 files changed

+102
-19
lines changed

5 files changed

+102
-19
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,4 @@ Thanks to
7070
* Erik Rose (erikrose) for a quick pyelasticsearch-compatibility patch
7171
* Stefan Wehrmeyer (stefanw) for a simple search filter fix
7272
* Dan Watson (dcwatson) for various patches.
73+
* Andrew Schoen (andrewschoen) for the addition of ``HAYSTACK_IDENTIFIER_METHOD``

docs/settings.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,3 +268,20 @@ An example::
268268
HAYSTACK_DJANGO_ID_FIELD = 'my_django_id'
269269

270270
Default is ``django_id``.
271+
272+
273+
``HAYSTACK_IDENTIFIER_METHOD``
274+
==============================
275+
276+
**Optional**
277+
278+
This setting allows you to provide a custom method for
279+
``haystack.utils.get_identifier``. Useful when the default identifier
280+
pattern of <app.label>.<object_name>.<pk> isn't suited to your
281+
needs.
282+
283+
An example::
284+
285+
HAYSTACK_IDENTIFIER_METHOD = 'my_app.module.get_identifier'
286+
287+
Default is ``haystack.utils.default_get_identifier``.

haystack/utils/__init__.py

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,77 @@
11
import re
2+
23
from haystack.constants import ID, DJANGO_CT, DJANGO_ID
34
from haystack.utils.highlighting import Highlighter
45

56

6-
IDENTIFIER_REGEX = re.compile('^[\w\d_]+\.[\w\d_]+\.\d+$')
7+
from django.conf import settings
78

9+
try:
10+
from django.utils import importlib
11+
except ImportError:
12+
import importlib
813

9-
def get_model_ct(model):
10-
return "%s.%s" % (model._meta.app_label, model._meta.module_name)
14+
IDENTIFIER_REGEX = re.compile('^[\w\d_]+\.[\w\d_]+\.\d+$')
1115

1216

13-
def get_identifier(obj_or_string):
17+
def default_get_identifier(obj_or_string):
1418
"""
1519
Get an unique identifier for the object or a string representing the
1620
object.
17-
21+
1822
If not overridden, uses <app_label>.<object_name>.<pk>.
1923
"""
2024
if isinstance(obj_or_string, basestring):
2125
if not IDENTIFIER_REGEX.match(obj_or_string):
22-
raise AttributeError("Provided string '%s' is not a valid identifier." % obj_or_string)
23-
26+
raise AttributeError(u"Provided string '%s' is not a valid identifier." % obj_or_string)
27+
2428
return obj_or_string
25-
26-
return u"%s.%s.%s" % (obj_or_string._meta.app_label, obj_or_string._meta.module_name, obj_or_string._get_pk_val())
29+
30+
return u"%s.%s.%s" % (
31+
obj_or_string._meta.app_label,
32+
obj_or_string._meta.module_name,
33+
obj_or_string._get_pk_val()
34+
)
35+
36+
37+
def _lookup_identifier_method():
38+
"""
39+
If the user has set HAYSTACK_IDENTIFIER_METHOD, import it and return the method uncalled.
40+
If HAYSTACK_IDENTIFIER_METHOD is not defined, return haystack.utils.default_get_identifier.
41+
42+
This always runs at module import time. We keep the code in a function
43+
so that it can be called from unit tests, in order to simulate the re-loading
44+
of this module.
45+
"""
46+
if not hasattr(settings, 'HAYSTACK_IDENTIFIER_METHOD'):
47+
return default_get_identifier
48+
49+
module_path, method_name = settings.HAYSTACK_IDENTIFIER_METHOD.rsplit(".", 1)
50+
51+
try:
52+
module = importlib.import_module(module_path)
53+
except ImportError:
54+
raise ImportError(u"Unable to import module '%s' provided for HAYSTACK_IDENTIFIER_METHOD." % module_path)
55+
56+
identifier_method = getattr(module, method_name, None)
57+
58+
if not identifier_method:
59+
raise AttributeError(
60+
u"Provided method '%s' for HAYSTACK_IDENTIFIER_METHOD does not exist in '%s'." % (method_name, module_path)
61+
)
62+
63+
return identifier_method
64+
65+
66+
get_identifier = _lookup_identifier_method()
67+
68+
69+
def get_model_ct(model):
70+
return "%s.%s" % (model._meta.app_label, model._meta.module_name)
2771

2872

2973
def get_facet_field_name(fieldname):
3074
if fieldname in [ID, DJANGO_ID, DJANGO_CT]:
3175
return fieldname
32-
76+
3377
return "%s_exact" % fieldname

tests/core/custom_identifier.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
def get_identifier_method(key):
3+
"""
4+
Custom get_identifier method used for testing the
5+
setting HAYSTACK_IDENTIFIER_MODULE
6+
"""
7+
return key

tests/core/tests/utils.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.test import TestCase
2+
from django.test.utils import override_settings
23
from haystack.utils import log
3-
from haystack.utils import get_identifier, get_facet_field_name, Highlighter
4+
from haystack.utils import get_identifier, get_facet_field_name, Highlighter, _lookup_identifier_method
45
from core.models import MockModel
56

67

@@ -16,45 +17,58 @@ def test_get_facet_field_name(self):
1617
class GetFacetFieldNameTestCase(TestCase):
1718
def test_get_identifier(self):
1819
self.assertEqual(get_identifier('core.mockmodel.1'), 'core.mockmodel.1')
19-
20+
2021
# Valid object.
2122
mock = MockModel.objects.get(pk=1)
2223
self.assertEqual(get_identifier(mock), 'core.mockmodel.1')
2324

25+
@override_settings(HAYSTACK_IDENTIFIER_METHOD='core.custom_identifier.get_identifier_method')
26+
def test_haystack_identifier_method(self):
27+
get_identifier = _lookup_identifier_method()
28+
self.assertEqual(get_identifier('a.b.c'), 'a.b.c')
29+
30+
@override_settings(HAYSTACK_IDENTIFIER_METHOD='core.custom_identifier.not_there')
31+
def test_haystack_identifier_method_bad_path(self):
32+
self.assertRaises(AttributeError, _lookup_identifier_method)
33+
34+
@override_settings(HAYSTACK_IDENTIFIER_METHOD='core.not_there.not_there')
35+
def test_haystack_identifier_method_bad_module(self):
36+
self.assertRaises(ImportError, _lookup_identifier_method)
37+
2438

2539
class HighlighterTestCase(TestCase):
2640
def setUp(self):
2741
super(HighlighterTestCase, self).setUp()
2842
self.document_1 = "This is a test of the highlightable words detection. This is only a test. Were this an actual emergency, your text would have exploded in mid-air."
2943
self.document_2 = "The content of words in no particular order causes nothing to occur."
3044
self.document_3 = "%s %s" % (self.document_1, self.document_2)
31-
45+
3246
def test_find_highlightable_words(self):
3347
highlighter = Highlighter('this test')
3448
highlighter.text_block = self.document_1
3549
self.assertEqual(highlighter.find_highlightable_words(), {'this': [0, 53, 79], 'test': [10, 68]})
36-
50+
3751
# We don't stem for now.
3852
highlighter = Highlighter('highlight tests')
3953
highlighter.text_block = self.document_1
4054
self.assertEqual(highlighter.find_highlightable_words(), {'highlight': [22], 'tests': []})
41-
55+
4256
# Ignore negated bits.
4357
highlighter = Highlighter('highlight -test')
4458
highlighter.text_block = self.document_1
4559
self.assertEqual(highlighter.find_highlightable_words(), {'highlight': [22]})
46-
60+
4761
def test_find_window(self):
4862
# The query doesn't matter for this method, so ignore it.
4963
highlighter = Highlighter('')
5064
highlighter.text_block = self.document_1
51-
65+
5266
# No query.
5367
self.assertEqual(highlighter.find_window({}), (0, 200))
54-
68+
5569
# Nothing found.
5670
self.assertEqual(highlighter.find_window({'highlight': [], 'tests': []}), (0, 200))
57-
71+
5872
# Simple cases.
5973
self.assertEqual(highlighter.find_window({'highlight': [0], 'tests': [100]}), (0, 200))
6074
self.assertEqual(highlighter.find_window({'highlight': [99], 'tests': [199]}), (99, 299))

0 commit comments

Comments
 (0)