[Python-modules-commits] [drf-haystack] 01/02: Imported Upstream version 1.5.2
Michael Fladischer
fladi at moszumanska.debian.org
Sun Aug 30 16:12:18 UTC 2015
This is an automated email from the git hooks/post-receive script.
fladi pushed a commit to branch master
in repository drf-haystack.
commit 3608abe12b9ae82c777f867cf478199ba2813725
Author: Michael Fladischer <FladischerMichael at fladi.at>
Date: Sun Aug 30 13:16:24 2015 +0200
Imported Upstream version 1.5.2
---
PKG-INFO | 20 +++
README.rst | 51 ++++++
drf_haystack.egg-info/PKG-INFO | 20 +++
drf_haystack.egg-info/SOURCES.txt | 15 ++
drf_haystack.egg-info/dependency_links.txt | 1 +
drf_haystack.egg-info/not-zip-safe | 1 +
drf_haystack.egg-info/pbr.json | 1 +
drf_haystack.egg-info/requires.txt | 3 +
drf_haystack.egg-info/top_level.txt | 1 +
drf_haystack/__init__.py | 10 ++
drf_haystack/fields.py | 85 ++++++++++
drf_haystack/filters.py | 255 +++++++++++++++++++++++++++++
drf_haystack/generics.py | 102 ++++++++++++
drf_haystack/serializers.py | 248 ++++++++++++++++++++++++++++
drf_haystack/viewsets.py | 36 ++++
setup.cfg | 5 +
setup.py | 60 +++++++
17 files changed, 914 insertions(+)
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..4daae50
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,20 @@
+Metadata-Version: 1.1
+Name: drf-haystack
+Version: 1.5.2
+Summary: Makes Haystack play nice with Django REST Framework
+Home-page: https://github.com/inonit/drf-haystack
+Author: Rolf Håvard Blindheim, Eirik Krogstad
+Author-email: rolf.blindheim at inonit.no, eirik.krogstad at inonit.no
+License: MIT License
+Download-URL: https://github.com/inonit/drf-haystack.git
+Description: Implements a ViewSet, some filters and serializers in order to play nice with Haystack.
+Platform: UNKNOWN
+Classifier: Operating System :: OS Independent
+Classifier: Development Status :: 4 - Beta
+Classifier: Environment :: Web Environment
+Classifier: Framework :: Django
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 3
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..a5e0ab7
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,51 @@
+Haystack for Django REST Framework
+==================================
+
+Build status
+------------
+
+.. image:: https://travis-ci.org/inonit/drf-haystack.svg?branch=master
+ :target: https://travis-ci.org/inonit/drf-haystack
+
+.. image:: https://readthedocs.org/projects/drf-haystack/badge/?version=latest
+ :target: https://readthedocs.org/projects/drf-haystack/?badge=latest
+ :alt: Documentation Status
+
+.. image:: https://pypip.in/d/drf-haystack/badge.png
+ :target: https://pypi.python.org/pypi/drf-haystack
+
+About
+-----
+
+Small library which tries to simplify integration of Haystack with Django REST Framework.
+Contains a Generic ViewSet, a Serializer and a couple of Filters in order to make search as
+painless as possible.
+
+Fresh `documentation available <http://drf-haystack.readthedocs.org/en/latest/>`_ on Read the docs!
+
+
+
+Supported Python and Django versions
+------------------------------------
+
+Tested with the following configurations:
+
+ - Python 2.6
+ - Django 1.5 and 1.6
+ - Python 2.7, 3.3 and 3.4
+ - Django 1.5, 1.6, 1.7 and 1.8
+
+Installation
+------------
+
+ $ pip install drf-haystack
+
+Supported features
+------------------
+We aim to support most features Haystack does (or at least those which can be used in a REST API).
+Currently we support:
+
+ * Autocomplete
+ * GEO Spatial searching
+ * Highlighting
+ * More Like This
diff --git a/drf_haystack.egg-info/PKG-INFO b/drf_haystack.egg-info/PKG-INFO
new file mode 100644
index 0000000..4daae50
--- /dev/null
+++ b/drf_haystack.egg-info/PKG-INFO
@@ -0,0 +1,20 @@
+Metadata-Version: 1.1
+Name: drf-haystack
+Version: 1.5.2
+Summary: Makes Haystack play nice with Django REST Framework
+Home-page: https://github.com/inonit/drf-haystack
+Author: Rolf Håvard Blindheim, Eirik Krogstad
+Author-email: rolf.blindheim at inonit.no, eirik.krogstad at inonit.no
+License: MIT License
+Download-URL: https://github.com/inonit/drf-haystack.git
+Description: Implements a ViewSet, some filters and serializers in order to play nice with Haystack.
+Platform: UNKNOWN
+Classifier: Operating System :: OS Independent
+Classifier: Development Status :: 4 - Beta
+Classifier: Environment :: Web Environment
+Classifier: Framework :: Django
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 3
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
diff --git a/drf_haystack.egg-info/SOURCES.txt b/drf_haystack.egg-info/SOURCES.txt
new file mode 100644
index 0000000..fcea38c
--- /dev/null
+++ b/drf_haystack.egg-info/SOURCES.txt
@@ -0,0 +1,15 @@
+README.rst
+setup.py
+drf_haystack/__init__.py
+drf_haystack/fields.py
+drf_haystack/filters.py
+drf_haystack/generics.py
+drf_haystack/serializers.py
+drf_haystack/viewsets.py
+drf_haystack.egg-info/PKG-INFO
+drf_haystack.egg-info/SOURCES.txt
+drf_haystack.egg-info/dependency_links.txt
+drf_haystack.egg-info/not-zip-safe
+drf_haystack.egg-info/pbr.json
+drf_haystack.egg-info/requires.txt
+drf_haystack.egg-info/top_level.txt
\ No newline at end of file
diff --git a/drf_haystack.egg-info/dependency_links.txt b/drf_haystack.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/drf_haystack.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/drf_haystack.egg-info/not-zip-safe b/drf_haystack.egg-info/not-zip-safe
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/drf_haystack.egg-info/not-zip-safe
@@ -0,0 +1 @@
+
diff --git a/drf_haystack.egg-info/pbr.json b/drf_haystack.egg-info/pbr.json
new file mode 100644
index 0000000..5de6075
--- /dev/null
+++ b/drf_haystack.egg-info/pbr.json
@@ -0,0 +1 @@
+{"git_version": "02394da", "is_release": false}
\ No newline at end of file
diff --git a/drf_haystack.egg-info/requires.txt b/drf_haystack.egg-info/requires.txt
new file mode 100644
index 0000000..cbf894d
--- /dev/null
+++ b/drf_haystack.egg-info/requires.txt
@@ -0,0 +1,3 @@
+Django>=1.5.0
+djangorestframework>=2.4.4
+django-haystack>=2.3.1
diff --git a/drf_haystack.egg-info/top_level.txt b/drf_haystack.egg-info/top_level.txt
new file mode 100644
index 0000000..1ccf5e6
--- /dev/null
+++ b/drf_haystack.egg-info/top_level.txt
@@ -0,0 +1 @@
+drf_haystack
diff --git a/drf_haystack/__init__.py b/drf_haystack/__init__.py
new file mode 100644
index 0000000..759a30e
--- /dev/null
+++ b/drf_haystack/__init__.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
+__title__ = "drf-haystack"
+__version__ = "1.5.2"
+__author__ = "Rolf Håvard Blindheim"
+__license__ = "MIT License"
+
+VERSION = __version__
diff --git a/drf_haystack/fields.py b/drf_haystack/fields.py
new file mode 100644
index 0000000..2338d46
--- /dev/null
+++ b/drf_haystack/fields.py
@@ -0,0 +1,85 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import, unicode_literals
+
+from rest_framework.fields import (
+ BooleanField, CharField, DateField, DateTimeField,
+ DecimalField, FloatField, IntegerField
+)
+
+
+class DRFHaystackFieldMixin(object):
+ prefix_field_names = False
+
+ def __init__(self, **kwargs):
+ self.prefix_field_names = kwargs.pop('prefix_field_names', False)
+ super(DRFHaystackFieldMixin, self).__init__(**kwargs)
+
+ def bind(self, field_name, parent):
+ """
+ Initializes the field name and parent for the field instance.
+ Called when a field is added to the parent serializer instance.
+ Taken from DRF and modified to support drf_haystack multiple index
+ functionality.
+ """
+
+ # In order to enforce a consistent style, we error if a redundant
+ # 'source' argument has been used. For example:
+ # my_field = serializer.CharField(source='my_field')
+ assert self.source != field_name, (
+ "It is redundant to specify `source='%s'` on field '%s' in "
+ "serializer '%s', because it is the same as the field name. "
+ "Remove the `source` keyword argument." %
+ (field_name, self.__class__.__name__, parent.__class__.__name__)
+ )
+
+ self.field_name = field_name
+ self.parent = parent
+
+ # `self.label` should default to being based on the field name.
+ if self.label is None:
+ self.label = field_name.replace('_', ' ').capitalize()
+
+ # self.source should default to being the same as the field name.
+ if self.source is None:
+ self.source = self.convert_field_name(field_name)
+
+ # self.source_attrs is a list of attributes that need to be looked up
+ # when serializing the instance, or populating the validated data.
+ if self.source == '*':
+ self.source_attrs = []
+ else:
+ self.source_attrs = self.source.split('.')
+
+ def convert_field_name(self, field_name):
+ if not self.prefix_field_names:
+ return field_name
+ return field_name.split("__")[-1]
+
+
+class HaystackBooleanField(DRFHaystackFieldMixin, BooleanField):
+ pass
+
+
+class HaystackCharField(DRFHaystackFieldMixin, CharField):
+ pass
+
+
+class HaystackDateField(DRFHaystackFieldMixin, DateField):
+ pass
+
+
+class HaystackDateTimeField(DRFHaystackFieldMixin, DateTimeField):
+ pass
+
+
+class HaystackDecimalField(DRFHaystackFieldMixin, DecimalField):
+ pass
+
+
+class HaystackFloatField(DRFHaystackFieldMixin, FloatField):
+ pass
+
+
+class HaystackIntegerField(DRFHaystackFieldMixin, IntegerField):
+ pass
diff --git a/drf_haystack/filters.py b/drf_haystack/filters.py
new file mode 100644
index 0000000..7669c74
--- /dev/null
+++ b/drf_haystack/filters.py
@@ -0,0 +1,255 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import, unicode_literals
+
+import operator
+import warnings
+from itertools import chain
+
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.utils import six
+
+import haystack
+from haystack.query import SearchQuerySet
+
+from rest_framework.filters import BaseFilterBackend
+
+
+class HaystackFilter(BaseFilterBackend):
+ """
+ A filter backend that compiles a haystack compatible
+ filtering query.
+ """
+
+ @staticmethod
+ def build_filter(view, filters=None):
+ """
+ Creates a single SQ filter from querystring parameters that
+ correspond to the SearchIndex fields that have been "registered"
+ in `view.fields`.
+
+ Default behavior is to `OR` terms for the same parameters, and `AND`
+ between parameters.
+
+ Any querystring parameters that are not registered in
+ `view.fields` will be ignored.
+ """
+
+ terms = []
+ exclude_terms = []
+
+ if filters is None:
+ filters = {} # pragma: no cover
+
+ for param, value in filters.items():
+ # Skip if the parameter is not listed in the serializer's `fields`
+ # or if it's in the `exclude` list.
+ excluding_term = False
+ param_parts = param.split("__")
+ base_param = param_parts[0] # only test against field without lookup
+ negation_keyword = getattr(settings, "DRF_HAYSTACK_NEGATION_KEYWORD", "not")
+ if len(param_parts) > 1 and param_parts[1] == negation_keyword:
+ excluding_term = True
+ param = param.replace("__%s" % negation_keyword, "") # haystack wouldn't understand our negation
+
+ if view.serializer_class:
+ try:
+ if hasattr(view.serializer_class.Meta, "field_aliases"):
+ old_base = base_param
+ base_param = view.serializer_class.Meta.field_aliases.get(base_param, base_param)
+ param = param.replace(old_base, base_param) # need to replace the alias
+
+ fields = getattr(view.serializer_class.Meta, "fields", [])
+ exclude = getattr(view.serializer_class.Meta, "exclude", [])
+ search_fields = getattr(view.serializer_class.Meta, "search_fields", [])
+
+ if ((fields or search_fields) and base_param not in chain(fields, search_fields)) or base_param in exclude or not value:
+ continue
+
+ except AttributeError:
+ raise ImproperlyConfigured("%s must implement a Meta class." %
+ view.serializer_class.__class__.__name__)
+
+ tokens = [token.strip() for token in value.split(view.lookup_sep)]
+ field_queries = []
+
+ for token in tokens:
+ if token:
+ field_queries.append(view.query_object((param, token)))
+
+ term = six.moves.reduce(operator.or_, filter(lambda x: x, field_queries))
+ if excluding_term:
+ exclude_terms.append(term)
+ else:
+ terms.append(term)
+
+ terms = six.moves.reduce(operator.and_, filter(lambda x: x, terms)) if terms else []
+ exclude_terms = six.moves.reduce(operator.and_, filter(lambda x: x, exclude_terms)) if exclude_terms else []
+ return (terms, exclude_terms)
+
+ def filter_queryset(self, request, queryset, view):
+ applicable_filters, applicable_exclusions = self.build_filter(view, filters=self.get_request_filters(request))
+ if applicable_filters:
+ queryset = queryset.filter(applicable_filters)
+ if applicable_exclusions:
+ queryset = queryset.exclude(applicable_exclusions)
+ return queryset
+
+ def get_request_filters(self, request):
+ return request.GET.copy()
+
+
+class HaystackAutocompleteFilter(HaystackFilter):
+ """
+ A filter backend to perform autocomplete search.
+
+ Must be run against fields that are either `NgramField` or
+ `EdgeNgramField`.
+ """
+
+ def filter_queryset(self, request, queryset, view):
+ """
+ Applying `applicable_filters` to the queryset by creating a
+ single SQ filter using `AND`.
+ """
+
+ applicable_filters, applicable_exclusions = self.build_filter(view, filters=self.get_request_filters(request))
+
+ if applicable_filters:
+ queryset = queryset.filter(self._construct_query(applicable_filters, queryset, view))
+ if applicable_exclusions:
+ queryset = queryset.exclude(self._construct_query(applicable_exclusions, queryset, view))
+
+ return queryset
+
+ def _construct_query(self, terms, queryset, view):
+ query_bits = []
+ for field_name, query in terms.children:
+ for word in query.split(" "):
+ bit = queryset.query.clean(word.strip())
+ kwargs = {
+ field_name: bit
+ }
+ query_bits.append(view.query_object(**kwargs))
+ return six.moves.reduce(operator.and_, filter(lambda x: x, query_bits))
+
+
+class HaystackGEOSpatialFilter(HaystackFilter):
+ """
+ A filter backend for doing geospatial filtering.
+ If using this filter make sure your index has a `LocationField`
+ named `coordinates`.
+
+ We'll always do the somewhat slower but more accurate `dwithin`
+ (radius) filter.
+ """
+
+ def __init__(self, *args, **kwargs):
+ try:
+ from haystack.utils.geo import D, Point
+ self.D = D
+ self.Point = Point
+ except ImportError as e: # pragma: no cover
+ warnings.warn("Make sure you've installed the `libgeos` library.\n "
+ "(`apt-get install libgeos` on linux, or `brew install geos` on OS X.)")
+ raise e
+
+ def unit_to_meters(self, distance_obj):
+ """
+ Emergency fix for https://github.com/toastdriven/django-haystack/issues/957
+ According to Elasticsearch documentation, units are always measured in meters unless
+ explicitly declared otherwise. It seems that the unit description is lost somewhere,
+ so everything ends up in the query without any unit values, thus the value is calculated
+ in meters.
+ """
+ return self.D(m=distance_obj.m * 1000) # pragma: no cover
+
+ def geo_filter(self, queryset, filters=None):
+ """
+ Filter the queryset by looking up parameters from the query
+ parameters.
+
+ Expected query parameters are:
+ - a `unit=value` parameter where the unit is a valid UNIT in the
+ `django.contrib.gis.measure.Distance` class.
+ - `from` which must be a comma separated longitude and latitude.
+
+ Example query:
+ /api/v1/search/?km=10&from=59.744076,10.152045
+
+ Will perform a `dwithin` query within 10 km from the point
+ with latitude 59.744076 and longitude 10.152045.
+ """
+
+ filters = dict((k, filters[k]) for k in chain(self.D.UNITS.keys(), ["from"]) if k in filters)
+ distance = dict((k, v) for k, v in filters.items() if k in self.D.UNITS.keys())
+ if "from" in filters and len(filters["from"].split(",")) == 2:
+ try:
+ latitude, longitude = map(float, filters["from"].split(","))
+ point = self.Point(longitude, latitude, srid=getattr(settings, "GEO_SRID", 4326))
+ if point and distance:
+ major, minor, _ = haystack.__version__
+ if queryset.query.backend.__class__.__name__ == "ElasticsearchSearchBackend" \
+ and (major == 2 and minor < 4):
+ distance = self.unit_to_meters(self.D(**distance)) # pragma: no cover
+ else:
+ distance = self.D(**distance)
+ queryset = queryset.dwithin("coordinates", point, distance).distance("coordinates", point)
+ except ValueError:
+ raise ValueError("Cannot convert `from=latitude,longitude` query parameter to "
+ "float values. Make sure to provide numerical values only!")
+
+ return queryset
+
+ def filter_queryset(self, request, queryset, view):
+ queryset = self.geo_filter(queryset, filters=request.GET.copy())
+ return super(HaystackGEOSpatialFilter, self).filter_queryset(request, queryset, view)
+
+
+class HaystackHighlightFilter(HaystackFilter):
+ """
+ A filter backend which adds support for ``highlighting`` on the
+ SearchQuerySet level (the fast one).
+ Note that you need to use a search backend which supports highlighting
+ in order to use this.
+
+ This will add a ``hightlighted`` entry to your response, encapsulating the
+ highlighted words in an `<em>highlighted results</em>` block.
+ """
+
+ def filter_queryset(self, request, queryset, view):
+ queryset = super(HaystackHighlightFilter, self).filter_queryset(request, queryset, view)
+ if request.GET and isinstance(queryset, SearchQuerySet):
+ queryset = queryset.highlight()
+ return queryset
+
+
+class HaystackBoostFilter(HaystackFilter):
+ """
+ Filter backend for applying term boost on query time.
+
+ Apply by adding a comma separated ``boost`` query parameter containing
+ a the term you want to boost and a floating point or integer for
+ the boost value. The boost value is based around ``1.0`` as 100% - no boost.
+
+ Gives a slight increase in relevance for documents that include "banana":
+ /api/v1/search/?boost=banana,1.1
+
+ The boost is applied *after* regular filtering has occurred.
+ """
+
+ @staticmethod
+ def apply_boost(queryset, filters):
+ if "boost" in filters and len(filters["boost"].split(",")) == 2:
+ term, boost = iter(filters["boost"].split(","))
+ try:
+ queryset = queryset.boost(term, float(boost))
+ except ValueError:
+ raise ValueError("Cannot convert boost to float value. Make sure to provide a "
+ "numerical boost value.")
+ return queryset
+
+ def filter_queryset(self, request, queryset, view):
+ queryset = super(HaystackBoostFilter, self).filter_queryset(request, queryset, view)
+ return self.apply_boost(queryset, filters=request.GET.copy())
diff --git a/drf_haystack/generics.py b/drf_haystack/generics.py
new file mode 100644
index 0000000..c177c82
--- /dev/null
+++ b/drf_haystack/generics.py
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import, unicode_literals
+
+import warnings
+
+from django.http import Http404
+
+from haystack.backends import SQ
+from haystack.query import SearchQuerySet
+from rest_framework.generics import GenericAPIView
+from rest_framework.permissions import AllowAny
+
+from .filters import HaystackFilter
+
+
+class HaystackGenericAPIView(GenericAPIView):
+ """
+ Base class for all haystack generic views.
+ """
+ # Use `index_models` to filter on which search index models we
+ # should include in the search result.
+ index_models = []
+
+ object_class = SearchQuerySet
+ query_object = SQ
+
+ # Override document_uid_field with whatever field in your index
+ # you use to uniquely identify a single document. This value will be
+ # used wherever the view references the `lookup_field` kwarg.
+ document_uid_field = "id"
+ lookup_sep = ","
+
+ #
+ # REST Framework overrides
+ #
+ filter_backends = [HaystackFilter]
+ permission_classes = [AllowAny]
+
+ def get_queryset(self):
+ """
+ Get the list of items for this view.
+ Returns ``self.queryset`` if defined and is a ``self.object_class``
+ instance.
+ """
+ if self.queryset and isinstance(self.queryset, self.object_class):
+ queryset = self.queryset.all()
+ else:
+ queryset = self.object_class()._clone()
+ if len(self.index_models):
+ queryset = queryset.models(*self.index_models)
+ return queryset
+
+ def get_object(self):
+ """
+ Fetch a single document from the data store according to whatever
+ unique identifier is available for that document in the
+ SearchIndex.
+ """
+ queryset = self.filter_queryset(self.get_queryset())
+ lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
+ if lookup_url_kwarg not in self.kwargs:
+ raise AttributeError(
+ "Expected view %s to be called with a URL keyword argument "
+ "named '%s'. Fix your URL conf, or set the `.lookup_field` "
+ "attribute on the view correctly." % (self.__class__.__name__, lookup_url_kwarg)
+ )
+ queryset = queryset.filter(self.query_object((self.document_uid_field, self.kwargs[lookup_url_kwarg])))
+ if queryset and len(queryset) == 1:
+ return queryset[0]
+ elif queryset and len(queryset) > 1:
+ raise Http404("Multiple results matches the given query. Expected a single result.")
+
+ raise Http404("No result matches the given query.")
+
+
+class SQHighlighterMixin(object):
+ """
+ DEPRECATED! Remove in v1.6.0.
+ Please use the HaystackHighlightFilter instead.
+
+ This mixin adds support for highlighting on the SearchQuerySet
+ level (the fast one).
+ Note that you need to use a backend which supports hightlighting in order
+ to use this.
+
+ This will add a `hightlighted` entry to your response, encapsulating the
+ highlighted words in an `<em>highlighted results</em>` block.
+ """
+ def filter_queryset(self, queryset):
+ warnings.warn(
+ "The SQHighlighterMixin is marked for deprecation, and has been re-written "
+ "as a filter backend. Please remove SQHighlighterMixin from the "
+ "%(cls)s, and add HaystackHighlightFilter to %(cls)s.filter_backends." %
+ {"cls": self.__class__.__name__},
+ DeprecationWarning
+ )
+
+ queryset = super(SQHighlighterMixin, self).filter_queryset(queryset)
+ if self.request.GET and isinstance(queryset, SearchQuerySet):
+ queryset = queryset.highlight()
+ return queryset
diff --git a/drf_haystack/serializers.py b/drf_haystack/serializers.py
new file mode 100644
index 0000000..c890b89
--- /dev/null
+++ b/drf_haystack/serializers.py
@@ -0,0 +1,248 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import, unicode_literals
+
+import copy
+import warnings
+from itertools import chain
+
+from django.core.exceptions import ImproperlyConfigured
+from django.utils import six
+
+from haystack import fields as haystack_fields
+from haystack.query import EmptySearchQuerySet
+from haystack.utils import Highlighter
+
+from rest_framework import serializers
+from rest_framework.compat import OrderedDict
+from rest_framework.fields import empty
+from rest_framework.utils.field_mapping import ClassLookupDict, get_field_kwargs
+
+from .fields import (
+ HaystackBooleanField, HaystackCharField, HaystackDateField, HaystackDateTimeField,
+ HaystackDecimalField, HaystackFloatField, HaystackIntegerField
+)
+
+
+class HaystackSerializer(serializers.Serializer):
+ """
+ A `HaystackSerializer` which populates fields based on
+ which models that are available in the SearchQueryset.
+ """
+ _field_mapping = ClassLookupDict({
+ haystack_fields.BooleanField: HaystackBooleanField,
+ haystack_fields.CharField: HaystackCharField,
+ haystack_fields.DateField: HaystackDateField,
+ haystack_fields.DateTimeField: HaystackDateTimeField,
+ haystack_fields.DecimalField: HaystackDecimalField,
+ haystack_fields.EdgeNgramField: HaystackCharField,
+ haystack_fields.FacetBooleanField: HaystackBooleanField,
+ haystack_fields.FacetCharField: HaystackCharField,
+ haystack_fields.FacetDateField: HaystackDateField,
+ haystack_fields.FacetDateTimeField: HaystackDateTimeField,
+ haystack_fields.FacetDecimalField: HaystackDecimalField,
+ haystack_fields.FacetFloatField: HaystackFloatField,
+ haystack_fields.FacetIntegerField: HaystackIntegerField,
+ haystack_fields.FacetMultiValueField: HaystackCharField,
+ haystack_fields.FloatField: HaystackFloatField,
+ haystack_fields.IntegerField: HaystackIntegerField,
+ haystack_fields.LocationField: HaystackCharField,
+ haystack_fields.MultiValueField: HaystackCharField,
+ haystack_fields.NgramField: HaystackCharField,
+ })
+
+ def __init__(self, instance=None, data=empty, **kwargs):
+ super(HaystackSerializer, self).__init__(instance, data, **kwargs)
+
+ try:
+ if not hasattr(self.Meta, "index_classes") and not hasattr(self.Meta, "serializers"):
+ raise ImproperlyConfigured("You must set either the 'index_classes' or 'serializers' "
+ "attribute on the serializer Meta class.")
+ except AttributeError:
+ raise ImproperlyConfigured("%s must implement a Meta class." % self.__class__.__name__)
+
+ if not self.instance:
+ self.instance = EmptySearchQuerySet()
+
+ @staticmethod
+ def _get_default_field_kwargs(model, field):
+ """
+ Get the required attributes from the model field in order
+ to instantiate a REST Framework serializer field.
+ """
+ kwargs = {}
+ if field.model_attr in model._meta.get_all_field_names():
+ model_field = model._meta.get_field_by_name(field.model_attr)[0]
+ kwargs = get_field_kwargs(field.model_attr, model_field)
+
+ # Remove stuff we don't care about!
+ delete_attrs = [
+ "allow_blank",
+ "choices",
+ "model_field",
+ ]
+ for attr in delete_attrs:
+ if attr in kwargs:
+ del kwargs[attr]
+
+ return kwargs
+
+ def get_fields(self):
+ """
+ Get the required fields for serializing the result.
+ """
+
+ fields = getattr(self.Meta, "fields", [])
+ exclude = getattr(self.Meta, "exclude", [])
+
+ if fields and exclude:
+ raise ImproperlyConfigured("Cannot set both `fields` and `exclude`.")
+
+ ignore_fields = getattr(self.Meta, "ignore_fields", [])
+ indices = getattr(self.Meta, "index_classes")
+
+ declared_fields = copy.deepcopy(self._declared_fields)
+ prefix_field_names = len(indices) > 1
+ field_mapping = OrderedDict()
+
+ # overlapping fields on multiple indices is supported by internally prefixing the field
+ # names with the index class to which they belong or, optionally, a user-provided alias
+ # for the index.
+ for index_cls in self.Meta.index_classes:
+ prefix = ""
+ if prefix_field_names:
+ prefix = "_%s__" % self._get_index_class_name(index_cls)
+ for field_name, field_type in six.iteritems(index_cls.fields):
+ orig_name = field_name
+ field_name = "%s%s" % (prefix, field_name)
+
+ # This has become a little more complex, but provides convenient flexibility for users
+ if not exclude:
+ if orig_name not in fields and field_name not in fields:
+ continue
+ elif orig_name in exclude or field_name in exclude or orig_name in ignore_fields or field_name in ignore_fields:
+ continue
+
+ # Look up the field attributes on the current index model,
+ # in order to correctly instantiate the serializer field.
+ model = index_cls().get_model()
+ kwargs = self._get_default_field_kwargs(model, field_type)
+ kwargs['prefix_field_names'] = prefix_field_names
+ field_mapping[field_name] = self._field_mapping[field_type](**kwargs)
+
+ # Add any explicitly declared fields. They *will* override any index fields
+ # in case of naming collision!.
+ if declared_fields:
+ for field_name in declared_fields:
+ if field_name in field_mapping:
+ warnings.warn("Field '{field}' already exists in the field list. This *will* "
+ "overwrite existing field '{field}'".format(field=field_name))
+ field_mapping[field_name] = declared_fields[field_name]
+ return field_mapping
+
+ def to_representation(self, instance):
+ """
+ If we have a serializer mapping, use that. Otherwise, use standard serializer behavior
+ Since we might be dealing with multiple indexes, some fields might
+ not be valid for all results. Do not render the fields which don't belong
+ to the search result.
+ """
+ if getattr(self.Meta, "serializers", None):
+ ret = self.multi_serializer_representation(instance)
+ else:
+ ret = super(HaystackSerializer, self).to_representation(instance)
+ prefix_field_names = len(getattr(self.Meta, "index_classes")) > 1
+ current_index = self._get_index_class_name(type(instance.searchindex))
+ for field in self.fields.keys():
+ orig_field = field
+ if prefix_field_names:
+ parts = field.split("__")
+ if len(parts) > 1:
+ index = parts[0][1:] # trim the preceding '_'
+ field = parts[1]
+ if index == current_index:
+ ret[field] = ret[orig_field]
+ del ret[orig_field]
+ elif field not in chain(instance.searchindex.fields.keys(), self._declared_fields.keys()):
+ del ret[orig_field]
+
+ # include the highlighted field in either case
+ if getattr(instance, "highlighted", None):
+ ret["highlighted"] = instance.highlighted[0]
+ return ret
+
+ def multi_serializer_representation(self, instance):
+ serializers = self.Meta.serializers
+ index = instance.searchindex
+ serializer_class = serializers.get(type(index), None)
+ if not serializer_class:
+ raise ImproperlyConfigured("Could not find serializer for %s in mapping" % index)
+ return serializer_class(context=self._context).to_representation(instance)
+
+ def _get_index_class_name(self, index_cls):
+ """
+ Converts in index model class to a name suitable for use as a field name prefix. A user
+ may optionally specify custom aliases via an 'index_aliases' attribute on the Meta class
+ """
+ cls_name = index_cls.__name__
+ aliases = getattr(self.Meta, "index_aliases", {})
+ return aliases.get(cls_name, cls_name.split('.')[-1])
+
+
+class HaystackSerializerMixin(object):
+ """
+ This mixin can be added to a rerializer to use the actual object as the data source for serialization rather
+ than the data stored in the search index fields. This makes it easy to return data from search results in
+ the same format as elswhere in your API and reuse your existing serializers
+ """
+
+ def to_representation(self, instance):
+ obj = instance.object
+ return super(HaystackSerializerMixin, self).to_representation(obj)
+
+
+class HighlighterMixin(object):
+ """
+ This mixin adds support for ``highlighting`` (the pure python, portable
+ version, not SearchQuerySet().highlight()). See Haystack docs
+ for more info).
+ """
+
+ highlighter_class = Highlighter
+ highlighter_css_class = "highlighted"
+ highlighter_html_tag = "span"
+ highlighter_max_length = 200
+ highlighter_field = None
+
+ def get_highlighter(self):
+ if not self.highlighter_class:
+ raise ImproperlyConfigured(
+ "%(cls)s is missing a highlighter_class. Define %(cls)s.highlighter_class, "
+ "or override %(cls)s.get_highlighter()." %
+ {"cls": self.__class__.__name__}
+ )
+ return self.highlighter_class
+
+ @staticmethod
+ def get_document_field(instance):
+ """
+ Returns which field the search index has marked as it's
+ `document=True` field.
+ """
+ for name, field in instance.searchindex.fields.items():
+ if field.document is True:
+ return name
+
+ def to_representation(self, instance):
+ ret = super(HighlighterMixin, self).to_representation(instance)
+ terms = " ".join(six.itervalues(self.context["request"].GET))
+ if terms:
+ highlighter = self.get_highlighter()(terms, **{
+ "html_tag": self.highlighter_html_tag,
+ "css_class": self.highlighter_css_class,
+ "max_length": self.highlighter_max_length
+ })
+ document_field = self.get_document_field(instance)
+ if highlighter and document_field:
+ ret["highlighted"] = highlighter.highlight(getattr(instance, self.highlighter_field or document_field))
+ return ret
diff --git a/drf_haystack/viewsets.py b/drf_haystack/viewsets.py
new file mode 100644
index 0000000..1a686f1
--- /dev/null
+++ b/drf_haystack/viewsets.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import, unicode_literals
+
+from rest_framework.decorators import detail_route
+from rest_framework.response import Response
+from rest_framework.viewsets import ViewSetMixin
+from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
+
+from .generics import HaystackGenericAPIView
+
+
+class HaystackViewSet(RetrieveModelMixin, ListModelMixin, ViewSetMixin, HaystackGenericAPIView):
+ """
+ The HaystackViewSet class provides the default `list()` and
+ `retrieve()` actions with a haystack index as it's data source.
+ """
+
+ @detail_route(methods=["get"], url_path="more-like-this")
+ def more_like_this(self, request, pk=None):
+ """
+ Sets up a detail route for ``more-like-this`` results.
+ Note that you'll need backend support in order to take advantage of this.
+
+ This will add ie. ^search/{pk}/more-like-this/$ to your existing ^search pattern.
+ """
+ queryset = self.filter_queryset(self.get_queryset())
+ mlt_queryset = queryset.more_like_this(self.get_object().object)
+
+ page = self.paginate_queryset(mlt_queryset)
+ if page is not None:
+ serializer = self.get_serializer(page, many=True)
+ return self.get_paginated_response(serializer.data)
+
+ serializer = self.get_serializer(mlt_queryset, many=True)
+ return Response(serializer.data)
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..ebbec92
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,5 @@
+[egg_info]
+tag_build =
+tag_svn_revision = 0
+tag_date = 0
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..4ab8eb3
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+
+import re
+import os
+
+try:
+ from setuptools import setup
+except ImportError:
+ from ez_setup import use_setuptools
+ use_setuptools()
+ from setuptools import setup
+
+
+def get_version(package):
+ """
+ Return package version as listed in `__version__` in `init.py`.
+ """
... 43 lines suppressed ...
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/drf-haystack.git
More information about the Python-modules-commits
mailing list