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