added order by feature as meta.

This commit is contained in:
Alejandro Nunez Capote 2019-06-04 15:17:32 -04:00
parent 25a5ceb2a8
commit 5c57ffccd7
6 changed files with 178 additions and 31 deletions

View File

@ -1,10 +1,25 @@
from elasticsearch_dsl.query import Query
from graphene_django.elasticsearch.filter.bridges import QuerysetBridge
from graphene_django.filter import DjangoFilterConnectionField
class DjangoESFilterConnectionField(DjangoFilterConnectionField):
"""A Field to replace DjangoFilterConnectionField manager by QuerysetBridge"""
def __init__(self, object_type, *args, **kwargs):
"""Validating field allowed for this connection"""
fields = kwargs.get('fields', None)
if fields is not None:
raise ValueError('DjangoESFilterConnectionField do not permit argument fields yet.')
order_by = kwargs.get('order_by', None)
if order_by is not None:
raise ValueError('DjangoESFilterConnectionField do not permit argument order_by yet.')
filterset_class = kwargs.get('filterset_class', None)
if filterset_class is None:
raise ValueError('You should provide a FilterSetES as filterset_class argument.')
super(DjangoESFilterConnectionField, self).__init__(object_type, *args, **kwargs)
def get_manager(self):
"""Returning a QuerysetBridge to replace the direct use over the QS"""

View File

@ -29,7 +29,7 @@ class StringFilterES(object): # pylint: disable=R0902
self.field_name = name or attr.replace('.', '_')
self.default_expr = default_expr or self.default_expr
self.lookup_expressions = lookup_expressions
self.argument = String().Argument()
self.argument = String()
self.fields = self.generate_fields()
def generate_fields(self):
@ -54,8 +54,9 @@ class StringFilterES(object): # pylint: disable=R0902
return fields
def get_q(self, arguments):
def generate_es_query(self, arguments):
"""
Generating a query based on the arguments passed to graphene field
:param arguments: parameters of the query.
:return: Returns a elasticsearch_dsl.Q query object.
"""
@ -77,3 +78,10 @@ class StringFilterES(object): # pylint: disable=R0902
queries.extend([query(self.field_name, value)])
return Q("bool", must=queries[0]) if len(queries) == 1 else Q("bool", must={"bool": {"should": queries}})
def Argument(self):
"""
Defining graphene Argument type for this filter
:return: A Argument type
"""
return self.argument.Argument()

View File

@ -2,8 +2,10 @@
import copy
from collections import OrderedDict
from elasticsearch_dsl import Q
from django_elasticsearch_dsl import ObjectField, StringField, TextField
from graphene import Enum, InputObjectType, Field
from django_elasticsearch_dsl import StringField, TextField
from django.utils import six
from django_filters.utils import try_dbfield
from django_filters.filterset import BaseFilterSet
@ -16,6 +18,19 @@ FILTER_FOR_ESFIELD_DEFAULTS = {
}
class OrderEnum(Enum):
"""Order enum to desc-asc"""
asc = 'asc'
desc = 'desc'
@property
def description(self):
"""Description to order enum"""
if self == OrderEnum.asc:
return 'Ascendant order'
return 'Descendant order'
class FilterSetESOptions(object):
"""Basic FilterSetES options to Metadata"""
def __init__(self, options=None):
@ -71,13 +86,33 @@ class FilterSetESOptions(object):
index = UserIndex
excludes = ['username', 'last_login']
It is necessary to provide includes or excludes. You cant provide a excludes empty to generate all fields
You can also pass sort_by to Meta to allow field be ordered
Example:
class UserFilter(FilterSetES):
class Meta:
index = UserIndex
excludes = []
order_by = ['username', 'last_login']
or
It is necessary to provide includes or excludes. You cant provide a excludes empty to generate all fields
class UserFilter(FilterSetES):
class Meta:
index = UserIndex
excludes = []
order_by = {
'username': user.name
'last_login': last_login
}
"""
self.index = getattr(options, 'index', None)
self.includes = getattr(options, 'includes', None)
self.excludes = getattr(options, 'excludes', None)
self.order_by = getattr(options, 'order_by', None)
if self.index is None:
raise ValueError('You need provide a Index in Meta.')
@ -101,11 +136,21 @@ class FilterSetESMetaclass(type):
if issubclass(new_class, BaseFilterSet):
new_class._meta = FilterSetESOptions(getattr(new_class, 'Meta', None))
base_filters = OrderedDict()
for name, filter_field in six.iteritems(declared_filters):
base_filters.update(filter_field.fields)
meta_filters = mcs.get_meta_filters(new_class._meta)
base_filters.update(OrderedDict(meta_filters))
base_filters.update(meta_filters)
sort_fields = {}
if new_class._meta.order_by is not None:
sort_fields = mcs.generate_sort_field(new_class._meta.order_by)
sort_type = mcs.create_sort_enum(name, sort_fields)
base_filters['sort'] = sort_type()
new_class.sort_fields = sort_fields
new_class.base_filters = base_filters
return new_class
@ -142,13 +187,11 @@ class FilterSetESMetaclass(type):
"""
index_fields = mcs.get_index_fields(meta)
meta_filters = []
meta_filters = OrderedDict()
for name, index_field, data in index_fields:
if isinstance(index_field, ObjectField):
meta_filters.extend((name, mcs.get_filter_object(name, index_field, data)))
else:
meta_filters.append((name, mcs.get_filter_exp(name, index_field, data)))
filter_class = mcs.get_filter_exp(name, index_field, data)
meta_filters.update(filter_class.fields)
return meta_filters
@ -201,13 +244,14 @@ class FilterSetESMetaclass(type):
kwargs = copy.deepcopy(extra)
# Get lookup_expr from configuration
if data and 'lookup_exprs' in data:
if 'lookup_exprs' in kwargs:
kwargs['lookup_exprs'] = set(kwargs['lookup_exprs']).intersection(set(data['lookup_exprs']))
if data and 'lookup_expressions' in data:
if 'lookup_expressions' in kwargs:
kwargs['lookup_expressions'] = set(kwargs['lookup_expressions'])\
.intersection(set(data['lookup_expressions']))
else:
kwargs['lookup_exprs'] = set(data['lookup_exprs'])
elif 'lookup_exprs' in kwargs:
kwargs['lookup_exprs'] = set(kwargs['lookup_exprs'])
kwargs['lookup_expressions'] = set(data['lookup_expressions'])
elif 'lookup_expressions' in kwargs:
kwargs['lookup_expressions'] = set(kwargs['lookup_expressions'])
kwargs['name'], kwargs['attr'] = mcs.get_name(name, root, data)
return filter_class(**kwargs)
@ -223,6 +267,49 @@ class FilterSetESMetaclass(type):
attr = '{root}.{name}'.format(root=root, name=name) if root else name
return field_name, attr
@staticmethod
def create_sort_enum(name, sort_fields):
"""
Create enum to sort by fields.
As graphene is typed, it is necessary generate a Enum by Field
to have inside, the document fields allowed to be ordered
"""
sort_enum_name = "{}SortFields".format(name)
sort_descriptions = {field: "Sort by {field}".format(field=field) for field in
sort_fields.keys()}
sort_fields = [(field, field) for field in sort_fields.keys()]
class EnumWithDescriptionsType(object):
"""Set description to enum fields"""
@property
def description(self):
"""Description to EnumSort"""
return sort_descriptions[self.name]
enum = Enum(sort_enum_name, sort_fields, type=EnumWithDescriptionsType)
class SortType(InputObjectType):
"""Sort Type"""
order = Field(OrderEnum)
field = Field(enum, required=True)
sort_name = "{}Sort".format(name)
sort_type = type(sort_name, (SortType,), {})
return sort_type
@staticmethod
def generate_sort_field(order_by):
"""To normalize the sort field data"""
if not order_by:
sort_fields = {}
elif isinstance(order_by, dict):
sort_fields = order_by.copy()
else:
sort_fields = {field: field for field in order_by}
return sort_fields
class FilterSetES(six.with_metaclass(FilterSetESMetaclass, object)):
"""FilterSet specific for ElasticSearch."""
@ -240,19 +327,34 @@ class FilterSetES(six.with_metaclass(FilterSetESMetaclass, object)):
@property
def qs(self):
"""Returning ES queryset as QS"""
query_base = self.generate_q()
query_base = self.generate_es_query()
self.es_query.apply_query("query", query_base)
self.es_query.apply_query("source", ["id"])
if 'sort' in self.data:
sort_data = self.data['sort'].copy()
field_name = self.sort_fields[sort_data.pop('field')]
self.es_query.apply_query("sort", {field_name: sort_data})
return self.es_query
def generate_q(self):
def generate_es_query(self):
"""
Generate a query for each filter.
:return: Generates a super query with bool as root, and combines all sub-queries from each argument.
"""
query_base = Q("bool")
for name, filter_es in six.iteritems(self.base_filters):
query_filter = filter_es.get_q(self.data) if len(self.data) else None
# if the query have data
if len(self.data):
# for each field passed to the query
for name in self.data:
filter_es = self.base_filters.get(name)
# If a target filter is en FilterEs
if isinstance(filter_es, StringFilterES):
# It is generated a query or response None if the filter don't have data
query_filter = filter_es.generate_es_query(self.data)
if query_filter is not None:
query_base += query_filter
return query_base

View File

@ -27,8 +27,9 @@ class ArticleFilterESAsField(FilterSetES):
"""Metaclass data"""
index = ArticleDocument
includes = []
order_by = ['id']
headline = filters.StringFilterES(attr='headline')
headline = filters.StringFilterES(attr='headline', lookup_expressions=['term', 'contain'])
class ArticleFilterESInMeta(FilterSetES):
@ -46,7 +47,7 @@ class ArticleFilterESInMetaDict(FilterSetES):
index = ArticleDocument
includes = {
'headline': {
'lookup_expressions': ['term', 'contains']
'lookup_expressions': ['term', 'contain']
}
}

View File

@ -41,7 +41,7 @@ def fake_data():
return a1, a2
def filter_generation(field, query_str, spected_arguments):
def filter_generation(field, query_str, expected_arguments, method_to_mock="query"):
a1, a2 = fake_data()
query = """
@ -64,14 +64,14 @@ def filter_generation(field, query_str, spected_arguments):
with mock.patch('django_elasticsearch_dsl.search.Search.count', mock_count),\
mock.patch('django_elasticsearch_dsl.search.Search.__getitem__', mock_slice),\
mock.patch('elasticsearch_dsl.Search.query', mock_query):
mock.patch("elasticsearch_dsl.Search.%s" % method_to_mock, mock_query):
schema = Schema(query=ESFilterQuery)
result = schema.execute(query)
assert not result.errors
mock_query.assert_called_with(filters.StringFilterES(attr='headline').get_q(spected_arguments))
mock_query.assert_called_with(expected_arguments)
assert len(result.data[field]["edges"]) == 2
assert result.data[field]["edges"][0]["node"]["headline"] == "a1"
@ -79,12 +79,33 @@ def filter_generation(field, query_str, spected_arguments):
def test_filter_as_field():
filter_generation("articlesAsField", "headline: \"A text\"", {"headline": "A text"})
filter_generation(
"articlesAsField",
"headline: \"A text\"",
filters.StringFilterES(attr='headline').generate_es_query({"headline": "A text"}),
)
def test_filter_as_field_order_by():
filter_generation(
"articlesAsField",
"headline: \"A text\", sort:{order:desc, field:id}",
{'id': {'order': 'desc'}},
"sort"
)
def test_filter_in_meta():
filter_generation("articlesInMeta", "headline: \"A text\"", {"headline": "A text"})
filter_generation(
"articlesInMeta",
"headline: \"A text\"",
filters.StringFilterES(attr='headline').generate_es_query({"headline": "A text"}),
)
def test_filter_in_meta_dict():
filter_generation("articlesInMetaDict", "headline: \"A text\"", {"headline": "A text"})
filter_generation(
"articlesInMetaDict",
"headline: \"A text\"",
filters.StringFilterES(attr='headline').generate_es_query({"headline": "A text"}),
)

View File

@ -18,7 +18,7 @@ def get_filtering_args_from_filterset(filterset_class, type):
field_type = convert_form_field(filter_field.field).Argument()
field_type.description = filter_field.label
else:
field_type = filter_field.argument
field_type = filter_field.Argument()
args[name] = field_type