Graphene-django: How would one implement totalCount or count on DjangoFilterConnectionField?

Created on 13 May 2019  路  11Comments  路  Source: graphql-python/graphene-django

With Connection, one can implement a count on edges like so:

from graphene import Connection, ConnectionField, Node, Int
from graphene_django import DjangoObjectType
from ..models import Place

class Thing_Type(DjangoObjectType):
    class Meta:
        model = Thing
        interfaces = (Node, )


class Thing_Connection(Connection):
    class Meta:
        node = Thing_Type
    count = Int()

    def resolve_count(root, info):
        return len(root.edges)


class Query(object):
    things = ConnectionField(Thing_Connection)

    def resolve_things(root, info, **kwargs):
        return Thing.objects.all()
   return len(root.edges)

Given that DjangoFilterConnectionField won't accept a Connection, but requires a DjangoObjectType, how would one implement an equivalent count?

Most helpful comment

@phalt, never mind. Got it. I'll add this to the wiki FAQ once editing is enabled. Turns out the trick is to subclass Connection and declare that as a connection_class on the node type. DjangoFilterConnectionField then uses that declared connection_class class seamlessly, like so:

from graphene import ObjectType, Connection, Node, Int
from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField
from ..models import Place


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

    total_count = Int()
    edge_count = Int()

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


class PlaceType(DjangoObjectType):
    class Meta:
        model = Place
        filter_fields = {
            'id':  ['exact', 'icontains'],
            'name': ['exact', 'icontains', 'istartswith', 'iendswith'],
            'date': ['exact', 'icontains', 'istartswith', 'iendswith'],
            'date_sort': ['exact', 'icontains', 'istartswith', 'iendswith'],
        }
        interfaces = (Node, )
        connection_class = ExtendedConnection


class Query(ObjectType):
    places = DjangoFilterConnectionField(PlaceType)

This allows, in my example here, querying:

{
  places(first: 2, name_Icontains: "Dallas" after:"YXJyYXljb25uZWN0aW9uOjE1") {
    totalCount
    edgeCount
    edges {
      cursor 
      node {
        id  
        name
      }
    }
  }
}

Which returns:

{
{
  "data": {
    "places": {
      "totalCount": 23,
      "edgeCount": 2,
      "edges": [
        {
          "cursor": "YXJyYXljb25uZWN0aW9uOjE2",
          "node": {
            "id": "UGxhY2VUeXBlOjUxOA==",
            "name": "Dallas, Texas, United States"
          }
        },
        {
          "cursor": "YXJyYXljb25uZWN0aW9uOjE3",
          "node": {
            "id": "UGxhY2VUeXBlOjU0Nw==",
            "name": "Dallas, Alabama, United States"
          }
        }
      ]
    }
  }
}

All 11 comments

Could you subclass DjangoFilterConnectionField?

Also, look at relay connections and pagination. You might be able to get your answer there.

@phalt, any pointers? I'm going through issues and docs across graphene and graphene-django, and there seems to be some confusion regarding a best practice approach here. This is another item I'll add to the FAQ when I get my head around, as I said, best practice. It's trivial to implement using Connection and ConnectionField, but DjangoFilterConnectionField is proving to be a bear.

@phalt, never mind. Got it. I'll add this to the wiki FAQ once editing is enabled. Turns out the trick is to subclass Connection and declare that as a connection_class on the node type. DjangoFilterConnectionField then uses that declared connection_class class seamlessly, like so:

from graphene import ObjectType, Connection, Node, Int
from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField
from ..models import Place


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

    total_count = Int()
    edge_count = Int()

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


class PlaceType(DjangoObjectType):
    class Meta:
        model = Place
        filter_fields = {
            'id':  ['exact', 'icontains'],
            'name': ['exact', 'icontains', 'istartswith', 'iendswith'],
            'date': ['exact', 'icontains', 'istartswith', 'iendswith'],
            'date_sort': ['exact', 'icontains', 'istartswith', 'iendswith'],
        }
        interfaces = (Node, )
        connection_class = ExtendedConnection


class Query(ObjectType):
    places = DjangoFilterConnectionField(PlaceType)

This allows, in my example here, querying:

{
  places(first: 2, name_Icontains: "Dallas" after:"YXJyYXljb25uZWN0aW9uOjE1") {
    totalCount
    edgeCount
    edges {
      cursor 
      node {
        id  
        name
      }
    }
  }
}

Which returns:

{
{
  "data": {
    "places": {
      "totalCount": 23,
      "edgeCount": 2,
      "edges": [
        {
          "cursor": "YXJyYXljb25uZWN0aW9uOjE2",
          "node": {
            "id": "UGxhY2VUeXBlOjUxOA==",
            "name": "Dallas, Texas, United States"
          }
        },
        {
          "cursor": "YXJyYXljb25uZWN0aW9uOjE3",
          "node": {
            "id": "UGxhY2VUeXBlOjU0Nw==",
            "name": "Dallas, Alabama, United States"
          }
        }
      ]
    }
  }
}

@changeling sorry I was being a bad contributor and replying on my phone. Your suggestion is great - wiki is now open :)

No worries! I'll see about adding this as well.

Added.

Hi guys,

[adding @changeling ]
I implemented exactly that for the project I am working on and here is the error I am getting:

  File "/XXXXXX/lib/python3.7/site-packages/graphql/type/typemap.py", line 109, in reducer
    field_map = type_.fields
  File "/XXXXXX/lib/python3.7/site-packages/graphql/pyutils/cached_property.py", line 22, in __get__
    value = obj.__dict__[self.func.__name__] = self.func(obj)
  File "/XXXXXX/lib/python3.7/site-packages/graphql/type/definition.py", line 198, in fields
    return define_field_map(self, self._fields)
  File "/XXXXXX/lib/python3.7/site-packages/graphql/type/definition.py", line 212, in define_field_map
    field_map = field_map()
  File "/XXXXXX/lib/python3.7/site-packages/graphene/types/typemap.py", line 275, in construct_fields_for_type
    map = self.reducer(map, field.type)
  File "/XXXXXX/lib/python3.7/site-packages/graphene/relay/connection.py", line 129, in type
    if is_node(connection_type):
  File "/XXXXXX/lib/python3.7/site-packages/graphene/relay/node.py", line 22, in is_node
    for i in objecttype._meta.interfaces:
AttributeError: 'NoneType' object has no attribute 'interfaces'

I am using python3.7, graphene_django==2.10.1 and django 2.2

I see tests for this in the code base. So, I am thinking that I am doing something wrong.

from graphene import Int
from graphene import Connection
from graphene import ObjectType
from graphene_django.filter import DjangoFilterConnectionField
from graphene import relay
from graphene_django import DjangoObjectType

from project.models.Segment import Segment

class SegmentConnection(Connection):
    """
        Connection for relay segments.
    """

    class Meta:
        abstract = True

    count = Int(description="Number of segments in the system", required=True)

    def resolve_count(self, info, **kwarg):
        return Segment.objects.count()


class SegmentNode(DjangoObjectType,):
    """
        Serialized representation of a segment.
    """

    class Meta:
        model = Segment
        fields = (
            "id",
            "identifier",
            "description",
        )

        interfaces = (relay.Node,)
        connection_class = SegmentConnection

class Query(ObjectType):
    segments = DjangoFilterConnectionField(SegmentNode,)

Thank you for your help!

Solution so that totalCount field is also discoverable by the schema:

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

    @classmethod
    def __init_subclass_with_meta__(cls, node=None, name=None, **options):
        result = super().__init_subclass_with_meta__(node=node, name=name, **options)
        cls._meta.fields["total_count"] = graphene.Field(
            type=graphene.Int,
            name="totalCount",
            description="Number of items in the queryset.",
            required=True,
            resolver=cls.resolve_total_count,
        )
        return result

    def resolve_total_count(self, *_) -> int:
        return self.iterable.count()


class Bla(DjangoObjectType):
    class Meta:
        connection_class = ExtendedConnection
query {
  something() {
    edges {
      node {
         ...
      }
    }
    totalCount,
    pageInfo {
      hasNextPage
    }
  }
}

Is this an official resolution to the issue? It looks like a very clever hack around the issue.

thx

Was this page helpful?
0 / 5 - 0 ratings

Related issues

nickhudkins picture nickhudkins  路  3Comments

dan-klasson picture dan-klasson  路  4Comments

mraak picture mraak  路  3Comments

amiyatulu picture amiyatulu  路  3Comments

hyusetiawan picture hyusetiawan  路  4Comments