diff --git a/graphene_django/elasticsearch/__init__.py b/graphene_django/elasticsearch/__init__.py new file mode 100644 index 0000000..31497d3 --- /dev/null +++ b/graphene_django/elasticsearch/__init__.py @@ -0,0 +1,9 @@ +import warnings +from ..utils import DJANGO_ELASTICSEARCH_DSL_INSTALLED + +if not DJANGO_ELASTICSEARCH_DSL_INSTALLED: + warnings.warn( + "Use of elasticsearch integration requires the django_elasticsearch_dsl package " + "be installed. You can do so using `pip install django_elasticsearch_dsl`", + ImportWarning, + ) diff --git a/graphene_django/elasticsearch/filter/__init__.py b/graphene_django/elasticsearch/filter/__init__.py new file mode 100644 index 0000000..1f318b6 --- /dev/null +++ b/graphene_django/elasticsearch/filter/__init__.py @@ -0,0 +1,9 @@ +import warnings +from ...utils import DJANGO_FILTER_INSTALLED + +if not DJANGO_FILTER_INSTALLED: + warnings.warn( + "Use of django elasticsearch filtering requires the django-filter package " + "be installed. You can do so using `pip install django-filter`", + ImportWarning, + ) diff --git a/graphene_django/elasticsearch/filter/filters.py b/graphene_django/elasticsearch/filter/filters.py new file mode 100644 index 0000000..b1d6b9d --- /dev/null +++ b/graphene_django/elasticsearch/filter/filters.py @@ -0,0 +1,45 @@ +"""Filters to ElasticSearch""" +from collections import OrderedDict +from django_filters import CharFilter +from elasticsearch_dsl import Q + + +class StringFilterES(object): # pylint: disable=R0902 + """String Fields specific to ElasticSearch.""" + + default_expr = 'contain' + filter_class = CharFilter + + variants = { + "contain": lambda name, value: Q('match', + **{name: { + "query": value, + "fuzziness": "auto" + }}), + + "term": lambda name, value: Q('term', **{name: value}), + } + + def __init__(self, name=None, attr=None): + """ + :param name: Name of the field. This is the name that will be exported. + :param attr: Path to the index attr that will be used as filter. + """ + assert name or attr, "At least the field name or the field attr should be passed" + self.field_name = name or attr.replace('.', '_') + self.fields = self.generate_fields() + + def generate_fields(self): + """ + All FilterSet objects should specify its fields for the introspection. + + :return: A mapping of field to Filter type of field with all the suffix + expressions combinations. + """ + fields = OrderedDict() + for variant in self.variants: + variant_name = self.field_name if variant in ["default", self.default_expr] \ + else "%s_%s" % (self.field_name, variant) + fields[variant_name] = self.filter_class(field_name=variant_name) + + return fields diff --git a/graphene_django/elasticsearch/filter/filterset.py b/graphene_django/elasticsearch/filter/filterset.py new file mode 100644 index 0000000..4bf15e9 --- /dev/null +++ b/graphene_django/elasticsearch/filter/filterset.py @@ -0,0 +1,55 @@ +"""Fields""" +from collections import OrderedDict +from django.utils import six +from django_filters.filterset import BaseFilterSet + +from .filters import StringFilterES + + +class FilterSetESMetaclass(type): + """Captures the meta class of the filterSet class.""" + + def __new__(mcs, name, bases, attrs): + """Get filters declared explicitly in the class""" + + declared_filters = mcs.get_declared_filters(bases, attrs) + attrs['declared_filters'] = declared_filters + + new_class = super(FilterSetESMetaclass, mcs).__new__(mcs, name, bases, attrs) + + if issubclass(new_class, BaseFilterSet): + base_filters = OrderedDict() + for name, filter_field in six.iteritems(declared_filters): + base_filters.update(filter_field.fields) + new_class.base_filters = base_filters + + return new_class + + @classmethod + def get_declared_filters(mcs, bases, attrs): + """ + Get the filters declared in the class. + :param bases: base classes of the current class + :param attrs: attributes captured to be included as metadata + :return: An OrderedDict of filter fields declared in the class as static fields. + """ + + # List of filters declared in the class as static fields. + filters = [ + (filter_name, attrs.pop(filter_name)) + for filter_name, obj in list(attrs.items()) + if isinstance(obj, StringFilterES) + ] + + # Merge declared filters from base classes + for base in reversed(bases): + if hasattr(base, 'declared_filters'): + filters = [(name, field) for name, field in base.declared_filters.items() if name not in attrs] \ + + filters + + return OrderedDict(filters) + + +class FilterSetES(six.with_metaclass(FilterSetESMetaclass, object)): + """FilterSet specific for ElasticSearch.""" + pass diff --git a/graphene_django/utils/__init__.py b/graphene_django/utils/__init__.py index f9c388d..73c871d 100644 --- a/graphene_django/utils/__init__.py +++ b/graphene_django/utils/__init__.py @@ -1,5 +1,6 @@ from .utils import ( DJANGO_FILTER_INSTALLED, + DJANGO_ELASTICSEARCH_DSL_INSTALLED, get_reverse_fields, maybe_queryset, get_model_fields, @@ -10,6 +11,7 @@ from .testing import GraphQLTestCase __all__ = [ "DJANGO_FILTER_INSTALLED", + "DJANGO_ELASTICSEARCH_DSL_INSTALLED", "get_reverse_fields", "maybe_queryset", "get_model_fields", diff --git a/graphene_django/utils/utils.py b/graphene_django/utils/utils.py index 02c47ee..5195e25 100644 --- a/graphene_django/utils/utils.py +++ b/graphene_django/utils/utils.py @@ -12,6 +12,14 @@ except ImportError: DJANGO_FILTER_INSTALLED = False +try: + import django_elasticsearch_dsl # noqa + + DJANGO_ELASTICSEARCH_DSL_INSTALLED = True +except ImportError: + DJANGO_ELASTICSEARCH_DSL_INSTALLED = False + + def get_reverse_fields(model, local_field_names): for name, attr in model.__dict__.items(): # Don't duplicate any local fields