From 15a6744aea95582798442c9b73a6f2f99c4101f5 Mon Sep 17 00:00:00 2001 From: Mardanov Timur Rustemovich Date: Thu, 29 Nov 2018 16:24:36 +0300 Subject: [PATCH] added filtering and parent node support --- graphene_django/fields.py | 45 ++++++++++++- graphene_django/filter/fields.py | 111 +++++++++++++++++++------------ graphene_django/filter/utils.py | 40 ++++++----- graphene_django/types.py | 7 +- 4 files changed, 142 insertions(+), 61 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 7ed6133..52aac01 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -97,7 +97,7 @@ class DjangoConnectionField(ConnectionField): return connection @classmethod - def connection_resolver( + def connection_resolver_original( cls, resolver, connection, @@ -137,6 +137,49 @@ class DjangoConnectionField(ConnectionField): return on_resolve(iterable) + @classmethod + def connection_resolver( + cls, + resolver, + connection, + default_manager, + max_limit, + enforce_first_or_last, + root, + info, + **args + ): + _parent = args.get('know_parent', False) + + if not _parent: + if hasattr(info.parent_type._meta, 'know_parent_fields'): + options = info.parent_type._meta.know_parent_fields + assert isinstance(options, (list, tuple)), \ + "know_parent_fields should be list or tuple" + _parent = info.field_name in options + + def new_resolver(root, info, **kwargs): + qs = resolver(root, info, **kwargs) + if qs is None: + qs = default_manager.filter() + if _parent and root is not None: + instances = [] + for instance in qs: + setattr(instance, '_parent', root) + instances.append(instance) + return instances + return qs + + return cls.connection_resolver_original( + new_resolver, + connection, + default_manager, + max_limit, + enforce_first_or_last, + root, + info, + **args) + def get_resolver(self, parent_resolver): return partial( self.connection_resolver, diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index cb42543..7034c7f 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -3,7 +3,8 @@ from functools import partial from graphene.types.argument import to_arguments from ..fields import DjangoConnectionField -from .utils import get_filtering_args_from_filterset, get_filterset_class +from .utils import get_filterset_class, get_filtering_args_from_filterset, \ + make_qs class DjangoFilterConnectionField(DjangoConnectionField): @@ -24,18 +25,15 @@ class DjangoFilterConnectionField(DjangoConnectionField): self._base_args = None super(DjangoFilterConnectionField, self).__init__(type, *args, **kwargs) - @property - def args(self): - return to_arguments(self._base_args or OrderedDict(), self.filtering_args) - - @args.setter - def args(self, args): - self._base_args = args - @property def filterset_class(self): if not self._filterset_class: - fields = self._fields or self.node_type._meta.filter_fields + if hasattr(self.node_type._meta, 'neomodel_filter_fields'): + fields = self.node_type._meta.neomodel_filter_fields + elif hasattr(self, '_fields'): + fields = self._fields + else: + fields = [] meta = dict(model=self.model, fields=fields) if self._extra_filter_meta: meta.update(self._extra_filter_meta) @@ -46,6 +44,14 @@ class DjangoFilterConnectionField(DjangoConnectionField): return self._filterset_class + @property + def args(self): + return to_arguments(self._base_args or OrderedDict(), self.filtering_args) + + @args.setter + def args(self, args): + self._base_args = args + @property def filtering_args(self): return get_filtering_args_from_filterset(self.filterset_class, self.node_type) @@ -74,38 +80,6 @@ class DjangoFilterConnectionField(DjangoConnectionField): queryset.query.set_limits(low, high) return queryset - @classmethod - def connection_resolver( - cls, - resolver, - connection, - default_manager, - max_limit, - enforce_first_or_last, - filterset_class, - filtering_args, - root, - info, - **args - ): - filter_kwargs = {k: v for k, v in args.items() if k in filtering_args} - qs = filterset_class( - data=filter_kwargs, - queryset=default_manager.get_queryset(), - request=info.context, - ).qs - - return super(DjangoFilterConnectionField, cls).connection_resolver( - resolver, - connection, - qs, - max_limit, - enforce_first_or_last, - root, - info, - **args - ) - def get_resolver(self, parent_resolver): return partial( self.connection_resolver, @@ -117,3 +91,56 @@ class DjangoFilterConnectionField(DjangoConnectionField): self.filterset_class, self.filtering_args, ) + + @classmethod + def connection_resolver(cls, + resolver, + connection, + default_manager, + max_limit, + enforce_first_or_last, + filterset_class, + filtering_args, + root, + info, + **args): + + order = args.get('order', None) + _parent = args.get('know_parent', False) + + if not _parent: + if hasattr(info.parent_type._meta, 'know_parent_fields'): + options = info.parent_type._meta.know_parent_fields + assert isinstance(options, (list, tuple)), \ + "know_parent_fields should be list or tuple" + _parent = info.field_name in options + + def new_resolver(root, info, **args): + filters = dict(filter(lambda x: '__' in x[0], args.items())) + qs = resolver(root, info, **args) + if qs is None: + qs = default_manager.filter() + + if filters: + qs = qs.filter(make_qs(filters)) + + if order: + qs = qs.order_by(order) + + if _parent and root is not None: + instances = [] + for instance in qs: + setattr(instance, '_parent', root) + instances.append(instance) + return instances + return qs + + return DjangoConnectionField.connection_resolver( + new_resolver, + connection, + default_manager, + max_limit, + enforce_first_or_last, + root, + info, + **args) diff --git a/graphene_django/filter/utils.py b/graphene_django/filter/utils.py index cfa5621..cf0da3a 100644 --- a/graphene_django/filter/utils.py +++ b/graphene_django/filter/utils.py @@ -1,23 +1,8 @@ import six from .filterset import custom_filterset_factory, setup_filterset - - -def get_filtering_args_from_filterset(filterset_class, type): - """ Inspect a FilterSet and produce the arguments to pass to - a Graphene Field. These arguments will be available to - filter against in the GraphQL - """ - from ..forms.converter import convert_form_field - - args = {} - for name, filter_field in six.iteritems(filterset_class.base_filters): - field_type = convert_form_field(filter_field.field).Argument() - field_type.description = filter_field.label - args[name] = field_type - - return args - +from functools import reduce +from neomodel import Q def get_filterset_class(filterset_class, **meta): """Get the class to be used as the FilterSet""" @@ -26,3 +11,24 @@ def get_filterset_class(filterset_class, **meta): # return it return setup_filterset(filterset_class) return custom_filterset_factory(**meta) + + +def make_qs(filters): + for item in filters.items(): + if '__equal' in item[0]: + filters.pop(item[0]) + filters[item[0].split("__")[0]] = item[1] + return reduce(lambda init, nx: init & Q(**{nx[0]: nx[1]}), filters.items(), Q()) + + +def get_filtering_args_from_filterset(filterset_class, type): + from ..forms.converter import convert_form_field + args = {} + fields = type._meta.model.defined_properties() + filterset_fields = filterset_class.Meta.fields or [] + for name in filterset_fields: + field_type = convert_form_field(fields[name]).Argument() + field_type.description = "filter" + for modificator in type._meta.neomodel_filter_fields[name]: + args["{}__{}".format(name, modificator)] = field_type + return args diff --git a/graphene_django/types.py b/graphene_django/types.py index 94a8344..0da2dfa 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -1,7 +1,7 @@ from collections import OrderedDict from django.utils.functional import SimpleLazyObject -from graphene import Field +from graphene import Field, Boolean from graphene.relay import Connection, Node from graphene.types.objecttype import ObjectType, ObjectTypeOptions from graphene.types.utils import yank_fields_from_attrs @@ -15,6 +15,9 @@ from neomodel import ( ) +KnowParent = dict(know_parent=Boolean(default_value=True)) + + def construct_fields(model, registry, only_fields, exclude_fields): _model_fields = get_model_fields(model) @@ -55,6 +58,7 @@ class DjangoObjectType(ObjectType): exclude_fields=(), filter_fields=None, neomodel_filter_fields=None, + know_parent_fields=None, connection=None, connection_class=None, use_connection=None, @@ -108,6 +112,7 @@ class DjangoObjectType(ObjectType): _meta.fields = django_fields _meta.connection = connection _meta.neomodel_filter_fields = neomodel_filter_fields + _meta.know_parent_fields = know_parent_fields super(DjangoObjectType, cls).__init_subclass_with_meta__( _meta=_meta, interfaces=interfaces, **options