Search code examples
pythondjangographqlgraphene-django

How to use Relay's pagination feature with filterset_class from django-filter when using graphene-django


I've a Django project where I'm using django-graphene to create a GraphQL API.

There's an issue when trying to use DjangoFilterConnectionField together with the relay.Connection (which is the core of pagination's feature)

My model is too large and has many relationships, but let's keep things simple...

class Pattern(models.Model):
    code = models.CharField(
        max_length=15
    )
    name = models.CharField(
        max_length=50
    )
    slug = AutoSlugField(
        populate_from='name',
        max_length=150
    )
    ...

My node looks like:

class PatternNode(DjangoObjectType):
    # Many fields here...
    ...

    class Meta:
        model = Pattern
        interfaces = (relay.Node,)
        filterset_class = PatternFilterSet

As you can see, I've setup the filterset_class attribute in the Meta of my Node.

So, here's that filter set:

class PatternFilterSet(FilterSet):
    order_by = OrderingFilter(
        fields=(
            ('date', 'date'),
            ('name', 'name'),
        )
    )
    productcategorization__design__contains = CharFilter(method="product_categorization_design_filter")
    products__predominant_colors__contains = CharFilter(method="products_predominant_colors_filter")

    class Meta:
        model = Pattern
        fields = {
            'name': ['exact', 'icontains', 'istartswith'],
            'alt_name': ['exact', 'icontains', 'istartswith'],
            'slug': ['exact'],
            'pattern_class': ['exact'],
            'sectors': ['exact', 'in'],
            'products__instances': ['exact'],
            'productcategorization__business': ['exact'],
            'productcategorization__market_segment': ['exact', 'in'],
        }

    @staticmethod
    def product_categorization_design_filter(queryset, name, value):
        """
        Does a productcategorization__design__contains filter "manually" because adding it in the Meta.fields does not
        work for ArrayField.

        Args:
             queryset (patterns.managers.PatternQuerySet)
             name (str)
             value (Array) comma delimited list of designs

        Returns:
            filtered_queryset (QuerySet)
        """
        return queryset.filter(productcategorization__design__contains=value.split(","))

    @staticmethod
    def products_predominant_colors_filter(queryset, name, value):
        """
        Does a products__predominant_colors__contains filter "manually" because adding it in the Meta.fields does not
        work for ArrayField.

        Args:
             queryset (patterns.managers.PatternQuerySet)
             name (str)
             value (Array) comma delimited list of designs

        Returns:
            filtered_queryset (QuerySet)
        """
        return queryset.filter(products__predominant_colors__contains=value.split(",")).distinct()

As you can see there are many special filtering options I need in my API for that particular Model.

In my schema I've the following:

class PatternConnection(relay.Connection):
    class Meta:
        node = PatternNode


class Query(graphene.ObjectType):
    pattern = relay.Node.Field(
        PatternNode,
        id=ID(),
        slug=String()
    )
    patterns = relay.ConnectionField(PatternConnection)

Everything works pretty fine at this point, but filters aren't working.

I'm executing the following query:

query Patterns {
    patterns(first: 2) {
        pageInfo {
            startCursor
            endCursor
            hasNextPage
        }
        edges {
            cursor
            node {
                id
                name
            }
        }
    }
}

and receiving the following response:

{
    "data": {
        "patterns": {
            "pageInfo": {
                "startCursor": "YXJyYXljb25uZWN0aW9uOjA=",
                "endCursor": "YXJyYXljb25uZWN0aW9uOjE=",
                "hasNextPage": true
            },
            "edges": [
                {
                    "cursor": "YXJyYXljb25uZWN0aW9uOjA=",
                    "node": {
                        "id": "UGF0dGVybk5vZGU6Mjcw",
                        "name": "42 Oz - Jk"
                    }
                },
                {
                    "cursor": "YXJyYXljb25uZWN0aW9uOjE=",
                    "node": {
                        "id": "UGF0dGVybk5vZGU6Mjcx",
                        "name": "42 Oz - Pebble Top - Jk"
                    }
                }
            ]
        }
    }
}

so pagination is working well!

Now, when I try it using one of my filters, like this:

query Patterns ($predominantColors: String) {
    patterns(first: 2, products_PredominantColors_Contains: $predominantColors) {
        pageInfo {
            startCursor
            endCursor
            hasNextPage
        }
        edges {
            cursor
            node {
                id
                name
            }
        }
    }
}

I'm receiving the following response:

{
    "errors": [
        {
            "message": "Unknown argument \"products_PredominantColors_Contains\" on field \"patterns\" of type \"Query\".",
            "locations": [
                {
                    "line": 2,
                    "column": 24
                }
            ]
        }
    ]
}

I assume, that's because I'm not using the DjangoFilterConnectionField as suggested here, but when I try to do this:

class PatternConnection(relay.Connection):
    class Meta:
        node = PatternNode


class Query(graphene.ObjectType):
    pattern = relay.Node.Field(
        PatternNode,
        id=ID(),
        slug=String()
    )
    patterns = DjangoFilterConnectionField(PatternConnection)

I'm getting the following error:

September 23, 2020 - 17:06:12
Django version 2.2.12, using settings 'proquinal_api.settings'
Starting development server at http://api.spradling.local:8000/
Quit the server with CONTROL-C.
/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene_django/types.py:131: UserWarning: Django model "cities_light.City" does not have a field or attribute named "location". Consider removing the field from the "exclude" list of DjangoObjectType "CityNode" because it has no effect
  type_=type_,
Internal Server Error: /graphql/
Traceback (most recent call last):
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene_django/settings.py", line 79, in import_from_string
    module = importlib.import_module(module_path)
  File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.7/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 728, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/Users/cristianrojas/www/spradling-api/proquinal_api/schema.py", line 49, in <module>
    mutation=Mutation
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene/types/schema.py", line 78, in __init__
    self.build_typemap()
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene/types/schema.py", line 168, in build_typemap
    initial_types, auto_camelcase=self.auto_camelcase, schema=self
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene/types/typemap.py", line 80, in __init__
    super(TypeMap, self).__init__(types)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphql/type/typemap.py", line 31, in __init__
    self.update(reduce(self.reducer, types, OrderedDict()))  # type: ignore
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene/types/typemap.py", line 88, in reducer
    return self.graphene_reducer(map, type)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene/types/typemap.py", line 117, in graphene_reducer
    return GraphQLTypeMap.reducer(map, internal_type)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphql/type/typemap.py", line 109, in reducer
    field_map = type_.fields
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphql/pyutils/cached_property.py", line 22, in __get__
    value = obj.__dict__[self.func.__name__] = self.func(obj)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphql/type/definition.py", line 198, in fields
    return define_field_map(self, self._fields)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphql/type/definition.py", line 212, in define_field_map
    field_map = field_map()
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene/types/typemap.py", line 275, in construct_fields_for_type
    map = self.reducer(map, field.type)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene_django/fields.py", line 98, in type
    assert _type._meta.connection, "The type {} doesn't have a connection".format(
AttributeError: 'ConnectionOptions' object has no attribute 'connection'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/django/core/handlers/base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/django/core/handlers/base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/django/views/generic/base.py", line 62, in view
    self = cls(**initkwargs)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene_django/views.py", line 100, in __init__
    schema = graphene_settings.SCHEMA
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene_django/settings.py", line 126, in __getattr__
    val = perform_import(val, attr)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene_django/settings.py", line 65, in perform_import
    return import_from_string(val, setting_name)
  File "/Users/cristianrojas/.virtualenvs/spradling-api-YJ5S1R6Y/lib/python3.7/site-packages/graphene_django/settings.py", line 88, in import_from_string
    raise ImportError(msg)
ImportError: Could not import 'proquinal_api.schema.schema' for Graphene setting 'SCHEMA'. AttributeError: 'ConnectionOptions' object has no attribute 'connection'.
[23/Sep/2020 17:08:02] "POST /graphql/ HTTP/1.1" 500 212017

So I'm wondering what's the right way to use DjangoFilterConnectionField in combination with my PatternConnection Relay's Connection to make filters and pagination work together.


Solution

  • pass the PatternNode to the DjangoFilterConnectionField as

    import graphene
    
    
    class PatternNode(DjangoObjectType):
        # Many fields here...
        ...
    
        class Meta:
            model = Pattern
            interfaces = (relay.Node,)
            filterset_class = PatternFilterSet
            
    
    class Query(graphene.ObjectType):
        patterns = DjangoFilterConnectionField(PatternNode)