Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filtering interface implementation #3952

Merged
merged 42 commits into from
Apr 24, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
a78b91f
Add initial implementation
Apr 10, 2019
4db4f56
Merge branch 'master' into 2588_filtering_interface
Apr 10, 2019
6b8d667
Restore back totalCount for products query
Apr 10, 2019
74299c9
Add param to change name of filter field
Apr 11, 2019
4a4b04d
Add test for checking generation of graphene input in FilterInputObje…
Apr 11, 2019
5ada57a
Change filter field from product_type__id to product_type
Apr 11, 2019
9811c0c
Add tests for products filters
Apr 11, 2019
7024601
Rename filters test variables
Apr 11, 2019
f3e6484
Add docs to FilterInput class
Apr 11, 2019
d1b26f9
Fix some codeclimate's issues
Apr 11, 2019
3e6da5c
Split file product.types into smaller files
Apr 12, 2019
2a79fe6
Apply changes after review. Change format of input fields
Apr 15, 2019
d83d9b5
Add tests for attributes filter
Apr 15, 2019
2ec5430
Add missing space
Apr 15, 2019
ce89ea1
Add tests for stock availability
Apr 16, 2019
e7459f9
Allow to use custom name of filters
Apr 16, 2019
41ed179
Rename filter_obj to filter_input
Apr 16, 2019
d8025b3
Merge branch 'master' into 2588_filtering_interface
Apr 16, 2019
399195c
Remove unneded base filter
Apr 16, 2019
45f7b2c
Apply changes after review
Apr 16, 2019
77f28b0
Add if condition to search filter
Apr 17, 2019
eeefa50
Add order filtering
Apr 18, 2019
58df428
Add filter for draft orders
Apr 18, 2019
6df2506
Remove unneeded import
Apr 18, 2019
45fa3d8
Remove merging queries
Apr 18, 2019
245ae3d
Add filter for customers
Apr 18, 2019
f15ee5e
Rename PriceInput to PriceRangeInput
Apr 18, 2019
57db03c
Add empty filters for vouchers
Apr 18, 2019
055e9fd
change quotation marks to single
Apr 18, 2019
cff32aa
Add rest of the filters for vouchers
Apr 18, 2019
7680aff
Fix empty lines in test_account
Apr 18, 2019
7238364
Add tests for vouchers with filter
Apr 18, 2019
a192b02
Add filters for sales
Apr 18, 2019
88339f9
Add filter for staff members query
Apr 23, 2019
3a7018a
Add filter for collection query
Apr 23, 2019
20cf436
Fix lines in graphql/product/filters
Apr 23, 2019
22219f3
Add filter for product_type query
Apr 23, 2019
7ab93b3
Apply changes after review
Apr 23, 2019
69d2c89
Merge branch 'master' into 2588_filtering_interface
Apr 23, 2019
ccb921e
Apply changes after review
Apr 24, 2019
6f8f195
Update changelog
Apr 24, 2019
84881bb
Merge branch 'master' into 2588_filtering_interface
Apr 24, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
45 changes: 45 additions & 0 deletions saleor/graphql/core/fields.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from functools import partial

import graphene
from django.db.models.query import QuerySet
from django_measurement.models import MeasurementField
Expand Down Expand Up @@ -66,3 +68,46 @@ def resolve_connection(cls, connection, default_manager, args, iterable):
connection.iterable = iterable
connection.length = _len
return connection


class FilterInputConnectionField(DjangoConnectionField):
def __init__(self, type, *args, **kwargs):
self.filters_obj = kwargs.get('filters')
self.filterset_class = None
if self.filters_obj:
self.filterset_class = self.filters_obj.filterset_class
super().__init__(type, *args, **kwargs)

@classmethod
def connection_resolver(
cls, resolver, connection, default_manager, max_limit,
enforce_first_or_last, filterset_class, root, info, **args):
filters_input = args.get('filters')
maarcingebala marked this conversation as resolved.
Show resolved Hide resolved
qs = default_manager
if filters_input and filterset_class:
qs = filterset_class(
data=dict(filters_input),
queryset=default_manager.get_queryset(),
request=info.context).qs

return super().connection_resolver(
resolver,
connection,
qs,
max_limit,
enforce_first_or_last,
root,
info,
**args
)

def get_resolver(self, parent_resolver):
return partial(
maarcingebala marked this conversation as resolved.
Show resolved Hide resolved
self.connection_resolver,
parent_resolver,
self.type,
self.get_manager(),
self.max_limit,
self.enforce_first_or_last,
self.filterset_class
)
12 changes: 12 additions & 0 deletions saleor/graphql/core/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import django_filters


class EnumFilter(django_filters.CharFilter):
"""
Filter for GraphQL's enum objects. enum_class stores graphQL enum
maarcingebala marked this conversation as resolved.
Show resolved Hide resolved
needed to generated schema. method needs to be always pass explicitly"""
def __init__(self, enum_class, *args, **kwargs):
assert kwargs.get('method'), ("Providing exact filter method is "
NyanKiyoshi marked this conversation as resolved.
Show resolved Hide resolved
"required for EnumFilter")
self.enum_class = enum_class
super().__init__(*args, **kwargs)
5 changes: 3 additions & 2 deletions saleor/graphql/core/types/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .common import (
CountryDisplay, Error, Image, LanguageDisplay, PermissionDisplay,
SeoInput, Weight)
CountryDisplay, Error, Image, LanguageDisplay, PermissionDisplay, SeoInput,
Weight)
from .filter_input import FilterInputObjectType
from .money import (
VAT, Money, MoneyRange, ReducedRate, TaxedMoney, TaxedMoneyRange)
from .upload import Upload
62 changes: 62 additions & 0 deletions saleor/graphql/core/types/filter_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import six
from graphene import InputField, InputObjectType
from graphene.types.inputobjecttype import InputObjectTypeOptions
from graphene.types.utils import yank_fields_from_attrs
from graphene_django.filter.utils import get_filterset_class
from graphene_django.forms.converter import convert_form_field

from saleor.graphql.core.filters import EnumFilter


class FilterInputObjectType(InputObjectType):
@classmethod
def __init_subclass_with_meta__(
cls, container=None, _meta=None, model=None, filterset_class=None,
fields=None, **options):
cls.custom_filterset_class = filterset_class
cls.filterset_class = None
cls.fields = fields
cls.model = model

if not _meta:
_meta = InputObjectTypeOptions(cls)

fields = cls.get_filtering_args_from_filterset()
fields = yank_fields_from_attrs(fields, _as=InputField)
if _meta.fields:
_meta.fields.update(fields)
else:
_meta.fields = fields

super().__init_subclass_with_meta__(_meta=_meta, **options)

@classmethod
def get_filtering_args_from_filterset(cls):
""" Inspect a FilterSet and produce the arguments to pass to
a Graphene Field. These arguments will be available to
filter against in the GraphQL
akjanik marked this conversation as resolved.
Show resolved Hide resolved
"""
if not cls.custom_filterset_class:
assert cls.model and cls.fields, (
"Provide filterset class or model and fields requested to "
NyanKiyoshi marked this conversation as resolved.
Show resolved Hide resolved
"create default filterset")

meta = dict(model=cls.model, fields=cls.fields)
cls.filterset_class = get_filterset_class(
cls.custom_filterset_class, **meta
)

args = {}
for name, filter_field in six.iteritems(
cls.filterset_class.base_filters):
enum_type = isinstance(filter_field, EnumFilter)
if enum_type:
field_type = filter_field.enum_class()
else:
field_type = convert_form_field(filter_field.field)
field_type.description = filter_field.label
kwargs = getattr(field_type, 'kwargs', {})
kwargs['name'] = name
field_type.kwargs = kwargs
args[name] = field_type
return args
16 changes: 15 additions & 1 deletion saleor/graphql/product/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
import operator
from collections import defaultdict

import django_filters
from django.db.models import Q

from ...product.models import Attribute
from ...product.models import Attribute, Product


def filter_products_by_attributes(qs, filter_value):
Expand Down Expand Up @@ -59,3 +60,16 @@ def sort_qs(qs, sort_by_product_order):
qs = qs.order_by(sort_by_product_order['direction']
+ sort_by_product_order['field'])
return qs


class ProductFilter(django_filters.FilterSet):
is_published = django_filters.BooleanFilter()

class Meta:
model = Product
fields = {
'name': ['exact', 'icontains'],
'category': ['exact'],
'product_type__id': ['exact'],
'price': ['exact', 'lt', 'gt'],
}
11 changes: 7 additions & 4 deletions saleor/graphql/product/schema.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from textwrap import dedent

import graphene
from graphene_django.filter import DjangoFilterConnectionField
from graphql_jwt.decorators import permission_required

from ..core.enums import ReportingPeriod
from ..core.fields import PrefetchingConnectionField
from ..core.fields import (
FilterInputConnectionField, PrefetchingConnectionField)
from ..descriptions import DESCRIPTIONS
from ..translations.mutations import (
AttributeTranslate, AttributeValueTranslate, CategoryTranslate,
Expand Down Expand Up @@ -35,8 +37,8 @@
resolve_products, resolve_report_product_sales)
from .scalars import AttributeScalar
from .types import (
Attribute, Category, Collection, DigitalContent, Product, ProductOrder,
ProductType, ProductVariant)
Attribute, Category, Collection, DigitalContent, Product,
ProductFilterInput, ProductOrder, ProductType, ProductVariant)


class ProductQueries(graphene.ObjectType):
Expand Down Expand Up @@ -76,8 +78,9 @@ class ProductQueries(graphene.ObjectType):
product = graphene.Field(
Product, id=graphene.Argument(graphene.ID, required=True),
description='Lookup a product by ID.')
products = PrefetchingConnectionField(
products = FilterInputConnectionField(
Product,
filters=ProductFilterInput(),
attributes=graphene.List(
AttributeScalar, description='Filter products by attributes.'),
categories=graphene.List(
Expand Down
10 changes: 9 additions & 1 deletion saleor/graphql/product/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
from ..core.connection import CountableDjangoObjectType
from ..core.enums import ReportingPeriod, TaxRateType
from ..core.fields import PrefetchingConnectionField
from ..core.types import Image, Money, MoneyRange, TaxedMoney, TaxedMoneyRange
from ..core.types import (
FilterInputObjectType, Image, Money, MoneyRange, TaxedMoney,
TaxedMoneyRange)
from ..translations.enums import LanguageCodeEnum
from ..translations.resolvers import resolve_translation
from ..translations.types import (
Expand All @@ -27,6 +29,7 @@
from ..utils import get_database_id, reporting_period_to_date
from .descriptions import AttributeDescriptions, AttributeValueDescriptions
from .enums import AttributeValueType, OrderDirection, ProductOrderField
from .filters import ProductFilter

COLOR_PATTERN = r'^(#[0-9a-fA-F]{3}|#(?:[0-9a-fA-F]{2}){2,4}|(rgb|hsl)a?\((-?\d+%?[,\s]+){2,3}\s*[\d\.]+%?\))$' # noqa
color_pattern = re.compile(COLOR_PATTERN)
Expand Down Expand Up @@ -301,6 +304,11 @@ class Meta:
description = 'Represents availability of a product in the storefront.'


class ProductFilterInput(FilterInputObjectType):
class Meta:
filterset_class = ProductFilter


class Product(CountableDjangoObjectType):
url = graphene.String(
description='The storefront URL for the product.', required=True)
Expand Down
13 changes: 12 additions & 1 deletion saleor/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1720,6 +1720,17 @@ type ProductDelete {
product: Product
}

input ProductFilterInput {
name: String
name__icontains: String
category: ID
product_type__id: ID
price: Float
price__lt: Float
maarcingebala marked this conversation as resolved.
Show resolved Hide resolved
price__gt: Float
is_published: Boolean
}

type ProductImage implements Node {
sortOrder: Int!
id: ID!
Expand Down Expand Up @@ -1969,7 +1980,7 @@ type Query {
collection(id: ID!): Collection
collections(query: String, before: String, after: String, first: Int, last: Int): CollectionCountableConnection
product(id: ID!): Product
products(attributes: [AttributeScalar], categories: [ID], collections: [ID], priceLte: Float, priceGte: Float, sortBy: ProductOrder, stockAvailability: StockAvailability, query: String, before: String, after: String, first: Int, last: Int): ProductCountableConnection
products(filters: ProductFilterInput, attributes: [AttributeScalar], categories: [ID], collections: [ID], priceLte: Float, priceGte: Float, sortBy: ProductOrder, stockAvailability: StockAvailability, query: String, before: String, after: String, first: Int, last: Int): ProductCountableConnection
productType(id: ID!): ProductType
productTypes(before: String, after: String, first: Int, last: Int): ProductTypeCountableConnection
productVariant(id: ID!): ProductVariant
Expand Down