Search code examples
djangographene-pythongraphene-django

Django Graphene Relay order_by (OrderingFilter)


I have a Graphene interface with Relay and filters. It works pretty well but I would like to add the order_by options. My objects look like:

    class FooGQLType(DjangoObjectType):
    class Meta:
        model = Foo
        exclude_fields = ('internal_id',)
        interfaces = (graphene.relay.Node,)
        filter_fields = {
            "id": ["exact"],
            "code": ["exact", "icontains"],
        }
        connection_class = ExtendedConnection

class Query(graphene.ObjectType):
    foo = DjangoFilterConnectionField(FooGQLType)

ExtendedConnection should not be relevant but:

class ExtendedConnection(graphene.Connection):
    class Meta:
        abstract = True

    total_count = graphene.Int()

    def resolve_total_count(root, info, **kwargs):
        return root.length

This allows me to query like foo(code_Icontains:"bar"). According to the Graphene documentation I should be using the OrderingFilter in a FilterSet for that. I find it a bit annoying since the filters are supposed to be automatic but if I do:

    class FooGQLFilter(FilterSet):
    class Meta:
        model = Foo

    order_by = OrderingFilter(
        fields=(
            ('code', 'code'),
            ('lastName', 'last_name'),
            ('otherNames', 'other_names'),
        )
    )

I get an error that I need to provide fields or exclude:

AssertionError: Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude' has been deprecated since 0.15.0 and is now disallowed. Add an explicit 'Meta.fields' or 'Meta.exclude' to the FooGQLFilter class.

So if I add a fields = [] to silence it, it compiles. However, when I use it in:

foo = DjangoFilterConnectionField(FooGQLType, filterset_class=FooGQLFilter)

My regular filters like code_Icontains vanish. I could add them again over there but it's silly. From a quick look at the source, it looks like Relay or django-filters already created a FilterSet class (makes sense) and overwriting it this way is obviously a poor idea.

How do I add the orderBy filter on my Graphene Relay filtered objects ? I feel like this should be pretty straightforward but I am struggling to figure this out.

I have also seen examples subclassing DjangoFilterConnectionField with a connection_resolver that injects the order_by somehow but that tells me that there is no orderBy parameter.


Solution

  • This solution only works with django-graphene until 2.6.0, see Alok Ramteke's solution for more recent versions

    I have adapted a solution from a GitHub issue on this topic:

    from graphene_django.filter import DjangoFilterConnectionField
    from graphene.utils.str_converters import to_snake_case
    
    
    class OrderedDjangoFilterConnectionField(DjangoFilterConnectionField):
        """
        Adapted from https://github.com/graphql-python/graphene/issues/251
        Substituting:
        `claims = DjangoFilterConnectionField(ClaimsGraphQLType)`
        with:
        ```
        claims = OrderedDjangoFilterConnectionField(ClaimsGraphQLType,
            orderBy=graphene.List(of_type=graphene.String))
        ```
        """
        @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
            order = args.get('orderBy', None)
            if order:
                if type(order) is str:
                    snake_order = to_snake_case(order)
                else:
                    snake_order = [to_snake_case(o) for o in order]
                qs = qs.order_by(*snake_order)
            return super(DjangoFilterConnectionField, cls).connection_resolver(
                resolver,
                connection,
                qs,
                max_limit,
                enforce_first_or_last,
                root,
                info,
                **args
            )
    

    To use it, just adapt the Query from:

    claims = DjangoFilterConnectionField(ClaimsGraphQLType)
    

    to

    claims = OrderedDjangoFilterConnectionField(ClaimsGraphQLType,
            orderBy=graphene.List(of_type=graphene.String))
    

    And you can then query:

    { claims(status: 2, orderBy: "-id") { id } }
    

    or

    { claims(status: 2, orderBy: ["creationDate", "lastName"]) { id } }