[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