1# encoding: utf-8
2from __future__ import absolute_import, division, print_function, unicode_literals
3
4import re
5
6from django.template import loader
7from django.utils import datetime_safe, six
8
9from haystack.exceptions import SearchFieldError
10from haystack.utils import get_model_ct_tuple
11
12from inspect import ismethod
13
14
15class NOT_PROVIDED:
16    pass
17
18# Note that dates in the full ISO 8601 format will be accepted as long as the hour/minute/second components
19# are zeroed for compatibility with search backends which lack a date time distinct from datetime:
20DATE_REGEX = re.compile(r'^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})(?:|T00:00:00Z?)$')
21DATETIME_REGEX = re.compile(r'^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})(T|\s+)(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2}).*?$')
22
23
24# All the SearchFields variants.
25
26class SearchField(object):
27    """The base implementation of a search field."""
28    field_type = None
29
30    def __init__(self, model_attr=None, use_template=False, template_name=None,
31                 document=False, indexed=True, stored=True, faceted=False,
32                 default=NOT_PROVIDED, null=False, index_fieldname=None,
33                 facet_class=None, boost=1.0, weight=None):
34        # Track what the index thinks this field is called.
35        self.instance_name = None
36        self.model_attr = model_attr
37        self.use_template = use_template
38        self.template_name = template_name
39        self.document = document
40        self.indexed = indexed
41        self.stored = stored
42        self.faceted = faceted
43        self._default = default
44        self.null = null
45        self.index_fieldname = index_fieldname
46        self.boost = weight or boost
47        self.is_multivalued = False
48
49        # We supply the facet_class for making it easy to create a faceted
50        # field based off of this field.
51        self.facet_class = facet_class
52
53        if self.facet_class is None:
54            self.facet_class = FacetCharField
55
56        self.set_instance_name(None)
57
58    def set_instance_name(self, instance_name):
59        self.instance_name = instance_name
60
61        if self.index_fieldname is None:
62            self.index_fieldname = self.instance_name
63
64    def has_default(self):
65        """Returns a boolean of whether this field has a default value."""
66        return self._default is not NOT_PROVIDED
67
68    @property
69    def default(self):
70        """Returns the default value for the field."""
71        if callable(self._default):
72            return self._default()
73
74        return self._default
75
76    def prepare(self, obj):
77        """
78        Takes data from the provided object and prepares it for storage in the
79        index.
80        """
81        # Give priority to a template.
82        if self.use_template:
83            return self.prepare_template(obj)
84        elif self.model_attr is not None:
85            attrs = self.split_model_attr_lookups()
86            current_objects = [obj]
87
88            values = self.resolve_attributes_lookup(current_objects, attrs)
89
90            if len(values) == 1:
91                return values[0]
92            elif len(values) > 1:
93                return values
94
95        if self.has_default():
96            return self.default
97        else:
98            return None
99
100    def resolve_attributes_lookup(self, current_objects, attributes):
101        """
102        Recursive method that looks, for one or more objects, for an attribute that can be multiple
103        objects (relations) deep.
104        """
105        values = []
106
107        for current_object in current_objects:
108            if not hasattr(current_object, attributes[0]):
109                raise SearchFieldError(
110                    "The model '%s' does not have a model_attr '%s'." % (repr(current_object), attributes[0])
111                )
112
113            if len(attributes) > 1:
114                current_objects_in_attr = self.get_iterable_objects(getattr(current_object, attributes[0]))
115                values.extend(self.resolve_attributes_lookup(current_objects_in_attr, attributes[1:]))
116                continue
117
118            current_object = getattr(current_object, attributes[0])
119
120            if current_object is None:
121                if self.has_default():
122                    current_object = self._default
123                elif self.null:
124                    current_object = None
125                else:
126                    raise SearchFieldError(
127                        "The model '%s' combined with model_attr '%s' returned None, but doesn't allow "
128                        "a default or null value." % (repr(current_object), self.model_attr)
129                    )
130
131            if callable(current_object):
132                values.append(current_object())
133            else:
134                values.append(current_object)
135
136        return values
137
138    def split_model_attr_lookups(self):
139        """Returns list of nested attributes for looking through the relation."""
140        return self.model_attr.split('__')
141
142    @classmethod
143    def get_iterable_objects(cls, current_objects):
144        """
145        Returns iterable of objects that contain data. For example, resolves Django ManyToMany relationship
146        so the attributes of the related models can then be accessed.
147        """
148        if current_objects is None:
149            return []
150
151        if hasattr(current_objects, 'all'):
152            # i.e, Django ManyToMany relationships
153            if ismethod(current_objects.all):
154                return current_objects.all()
155            return []
156
157        elif not hasattr(current_objects, '__iter__'):
158            current_objects = [current_objects]
159
160        return current_objects
161
162    def prepare_template(self, obj):
163        """
164        Flattens an object for indexing.
165
166        This loads a template
167        (``search/indexes/{app_label}/{model_name}_{field_name}.txt``) and
168        returns the result of rendering that template. ``object`` will be in
169        its context.
170        """
171        if self.instance_name is None and self.template_name is None:
172            raise SearchFieldError("This field requires either its instance_name variable to be populated or an explicit template_name in order to load the correct template.")
173
174        if self.template_name is not None:
175            template_names = self.template_name
176
177            if not isinstance(template_names, (list, tuple)):
178                template_names = [template_names]
179        else:
180            app_label, model_name = get_model_ct_tuple(obj)
181            template_names = ['search/indexes/%s/%s_%s.txt' % (app_label, model_name, self.instance_name)]
182
183        t = loader.select_template(template_names)
184        return t.render({'object': obj})
185
186    def convert(self, value):
187        """
188        Handles conversion between the data found and the type of the field.
189
190        Extending classes should override this method and provide correct
191        data coercion.
192        """
193        return value
194
195
196class CharField(SearchField):
197    field_type = 'string'
198
199    def __init__(self, **kwargs):
200        if kwargs.get('facet_class') is None:
201            kwargs['facet_class'] = FacetCharField
202
203        super(CharField, self).__init__(**kwargs)
204
205    def prepare(self, obj):
206        return self.convert(super(CharField, self).prepare(obj))
207
208    def convert(self, value):
209        if value is None:
210            return None
211
212        return six.text_type(value)
213
214
215class LocationField(SearchField):
216    field_type = 'location'
217
218    def prepare(self, obj):
219        from haystack.utils.geo import ensure_point
220
221        value = super(LocationField, self).prepare(obj)
222
223        if value is None:
224            return None
225
226        pnt = ensure_point(value)
227        pnt_lng, pnt_lat = pnt.coords
228        return "%s,%s" % (pnt_lat, pnt_lng)
229
230    def convert(self, value):
231        from haystack.utils.geo import ensure_point, Point
232
233        if value is None:
234            return None
235
236        if hasattr(value, 'geom_type'):
237            value = ensure_point(value)
238            return value
239
240        if isinstance(value, six.string_types):
241            lat, lng = value.split(',')
242        elif isinstance(value, (list, tuple)):
243            # GeoJSON-alike
244            lat, lng = value[1], value[0]
245        elif isinstance(value, dict):
246            lat = value.get('lat', 0)
247            lng = value.get('lon', 0)
248        else:
249            raise TypeError('Unable to extract coordinates from %r' % value)
250
251        value = Point(float(lng), float(lat))
252        return value
253
254
255class NgramField(CharField):
256    field_type = 'ngram'
257
258    def __init__(self, **kwargs):
259        if kwargs.get('faceted') is True:
260            raise SearchFieldError("%s can not be faceted." % self.__class__.__name__)
261
262        super(NgramField, self).__init__(**kwargs)
263
264
265class EdgeNgramField(NgramField):
266    field_type = 'edge_ngram'
267
268
269class IntegerField(SearchField):
270    field_type = 'integer'
271
272    def __init__(self, **kwargs):
273        if kwargs.get('facet_class') is None:
274            kwargs['facet_class'] = FacetIntegerField
275
276        super(IntegerField, self).__init__(**kwargs)
277
278    def prepare(self, obj):
279        return self.convert(super(IntegerField, self).prepare(obj))
280
281    def convert(self, value):
282        if value is None:
283            return None
284
285        return int(value)
286
287
288class FloatField(SearchField):
289    field_type = 'float'
290
291    def __init__(self, **kwargs):
292        if kwargs.get('facet_class') is None:
293            kwargs['facet_class'] = FacetFloatField
294
295        super(FloatField, self).__init__(**kwargs)
296
297    def prepare(self, obj):
298        return self.convert(super(FloatField, self).prepare(obj))
299
300    def convert(self, value):
301        if value is None:
302            return None
303
304        return float(value)
305
306
307class DecimalField(SearchField):
308    field_type = 'string'
309
310    def __init__(self, **kwargs):
311        if kwargs.get('facet_class') is None:
312            kwargs['facet_class'] = FacetDecimalField
313
314        super(DecimalField, self).__init__(**kwargs)
315
316    def prepare(self, obj):
317        return self.convert(super(DecimalField, self).prepare(obj))
318
319    def convert(self, value):
320        if value is None:
321            return None
322
323        return six.text_type(value)
324
325
326class BooleanField(SearchField):
327    field_type = 'boolean'
328
329    def __init__(self, **kwargs):
330        if kwargs.get('facet_class') is None:
331            kwargs['facet_class'] = FacetBooleanField
332
333        super(BooleanField, self).__init__(**kwargs)
334
335    def prepare(self, obj):
336        return self.convert(super(BooleanField, self).prepare(obj))
337
338    def convert(self, value):
339        if value is None:
340            return None
341
342        return bool(value)
343
344
345class DateField(SearchField):
346    field_type = 'date'
347
348    def __init__(self, **kwargs):
349        if kwargs.get('facet_class') is None:
350            kwargs['facet_class'] = FacetDateField
351
352        super(DateField, self).__init__(**kwargs)
353
354    def prepare(self, obj):
355        return self.convert(super(DateField, self).prepare(obj))
356
357    def convert(self, value):
358        if value is None:
359            return None
360
361        if isinstance(value, six.string_types):
362            match = DATE_REGEX.search(value)
363
364            if match:
365                data = match.groupdict()
366                return datetime_safe.date(int(data['year']), int(data['month']), int(data['day']))
367            else:
368                raise SearchFieldError("Date provided to '%s' field doesn't appear to be a valid date string: '%s'" % (self.instance_name, value))
369
370        return value
371
372
373class DateTimeField(SearchField):
374    field_type = 'datetime'
375
376    def __init__(self, **kwargs):
377        if kwargs.get('facet_class') is None:
378            kwargs['facet_class'] = FacetDateTimeField
379
380        super(DateTimeField, self).__init__(**kwargs)
381
382    def prepare(self, obj):
383        return self.convert(super(DateTimeField, self).prepare(obj))
384
385    def convert(self, value):
386        if value is None:
387            return None
388
389        if isinstance(value, six.string_types):
390            match = DATETIME_REGEX.search(value)
391
392            if match:
393                data = match.groupdict()
394                return datetime_safe.datetime(int(data['year']), int(data['month']), int(data['day']), int(data['hour']), int(data['minute']), int(data['second']))
395            else:
396                raise SearchFieldError("Datetime provided to '%s' field doesn't appear to be a valid datetime string: '%s'" % (self.instance_name, value))
397
398        return value
399
400
401class MultiValueField(SearchField):
402    field_type = 'string'
403
404    def __init__(self, **kwargs):
405        if kwargs.get('facet_class') is None:
406            kwargs['facet_class'] = FacetMultiValueField
407
408        if kwargs.get('use_template') is True:
409            raise SearchFieldError("'%s' fields can not use templates to prepare their data." % self.__class__.__name__)
410
411        super(MultiValueField, self).__init__(**kwargs)
412        self.is_multivalued = True
413
414    def prepare(self, obj):
415        return self.convert(super(MultiValueField, self).prepare(obj))
416
417    def convert(self, value):
418        if value is None:
419            return None
420
421        if hasattr(value, '__iter__') and not isinstance(value, six.text_type):
422            return value
423
424        return [value]
425
426
427class FacetField(SearchField):
428    """
429    ``FacetField`` is slightly different than the other fields because it can
430    work in conjunction with other fields as its data source.
431
432    Accepts an optional ``facet_for`` kwarg, which should be the field name
433    (not ``index_fieldname``) of the field it should pull data from.
434    """
435    instance_name = None
436
437    def __init__(self, **kwargs):
438        handled_kwargs = self.handle_facet_parameters(kwargs)
439        super(FacetField, self).__init__(**handled_kwargs)
440
441    def handle_facet_parameters(self, kwargs):
442        if kwargs.get('faceted', False):
443            raise SearchFieldError("FacetField (%s) does not accept the 'faceted' argument." % self.instance_name)
444
445        if not kwargs.get('null', True):
446            raise SearchFieldError("FacetField (%s) does not accept False for the 'null' argument." % self.instance_name)
447
448        if not kwargs.get('indexed', True):
449            raise SearchFieldError("FacetField (%s) does not accept False for the 'indexed' argument." % self.instance_name)
450
451        if kwargs.get('facet_class'):
452            raise SearchFieldError("FacetField (%s) does not accept the 'facet_class' argument." % self.instance_name)
453
454        self.facet_for = None
455        self.facet_class = None
456
457        # Make sure the field is nullable.
458        kwargs['null'] = True
459
460        if 'facet_for' in kwargs:
461            self.facet_for = kwargs['facet_for']
462            del(kwargs['facet_for'])
463
464        return kwargs
465
466    def get_facet_for_name(self):
467        return self.facet_for or self.instance_name
468
469
470class FacetCharField(FacetField, CharField):
471    pass
472
473
474class FacetIntegerField(FacetField, IntegerField):
475    pass
476
477
478class FacetFloatField(FacetField, FloatField):
479    pass
480
481
482class FacetDecimalField(FacetField, DecimalField):
483    pass
484
485
486class FacetBooleanField(FacetField, BooleanField):
487    pass
488
489
490class FacetDateField(FacetField, DateField):
491    pass
492
493
494class FacetDateTimeField(FacetField, DateTimeField):
495    pass
496
497
498class FacetMultiValueField(FacetField, MultiValueField):
499    pass
500