mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-07-13 17:52:19 +03:00
added order by feature as meta.
This commit is contained in:
parent
25a5ceb2a8
commit
5c57ffccd7
|
@ -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"""
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 query_filter is not None:
|
||||
query_base += query_filter
|
||||
# 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
|
||||
|
|
|
@ -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']
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"}),
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user