Search code examples
pythondjangographqlgraphene-pythongraphene-django

Using arguments to nested structures for filtering parents using django-graphene


I'm currently using Python & Django with Graphene to model my Graphql backend. My question is similar to this one on Graphene's repo https://github.com/graphql-python/graphene/issues/431. I'm also using graphene_django_optimizer library to improve performance.

However I'm having a hard time understanding how to apply @syrusakbary proposed solution to my problem in my current scenario. Any help would be much appreciated

This is the query I wanna execute

    getUser(userId: $userId)
    {
        id
        username
        trainings{
            id
            name
            sessions{
                id    
                createdAt
                completedAt
                category
            }
        }
    }
}

The trainings are brought correctly, only the ones belonging to that specific user id. However all sessions for each training are brought for all users. I'd like sessions to also be specific to that single user. Here on my types.py are the relevant types

class SessionType(DjangoObjectType):

    class Meta:
        model = Session
        fields = "__all__"
        convert_choices_to_enum = False

    @classmethod
    def get_queryset(cls, queryset, info, **kwargs):

        if info.context.user.is_anonymous:
            return queryset.order_by('-id')
        return queryset
class TrainingType(gql_optimizer.OptimizedDjangoObjectType):

    class Meta:
        model = Training
        fields = "__all__"
        convert_choices_to_enum = False
class UserType(DjangoObjectType):
    class Meta:
        model = get_user_model()
        fields = "__all__"

Here are my relevant models:

class Training(models.Model):
    name = models.CharField(max_length=200, help_text='Training\'s name')
    details = models.TextField(default="", help_text='Descriptive details about the training')
    course = models.ForeignKey("Course", related_name="trainings", on_delete=models.CASCADE)
    user = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="trainings")
    metric_cards = models.TextField(default="", help_text="An object describing the metrics to be used. (Optional)")

    def __str__(self):
        return str(self.id) + ' - ' + self.name


class Session(models.Model):
    name = models.CharField(max_length=200, help_text='Session\'s name')
    category = models.CharField(max_length=240, choices=SESSION_CATEGORIES, default="practice",
                                help_text='Session type. Can be of \'assessment\''
                                          'or \'practice\'')
    total_steps = models.IntegerField(default=1, help_text='Amount of steps for this session')
    created_at = models.DateTimeField(editable=False, default=timezone.now, help_text='Time the session was created'
                                                                                      '(Optional - default=now)')
    completed_at = models.DateTimeField(editable=False, null=True, blank=True, help_text='Time the session was finished'
                                                                                         '(Optional - default=null)')
    user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="training_sessions", on_delete=models.DO_NOTHING)
    training = models.ForeignKey("Training", related_name="sessions", on_delete=models.CASCADE)

    def __str__(self):
        return self.name

My resolvers are in separated files, according to each type so for example class SessionQuery(graphene.ObjectType) contains all resolvers related to session. Something like this:

class SessionQuery(graphene.ObjectType):
    debug = graphene.Field(DjangoDebug, name='_debug')

    # Session Queries
    session = graphene.Field(SessionType, id=graphene.Int(), user_id=graphene.Int(required=True))
    last_created_session = graphene.Field(SessionType)
    all_sessions = graphene.List(SessionType,
                                 first=graphene.Int(),
                                 skip=graphene.Int(),)
    latest_activities = graphene.List(LatestActivity, limit=graphene.Int())
    activity_in_range = graphene.List(UserRangeActivity, start_date=graphene.Date(), end_date=graphene.Date())
    unique_users_activity_in_range = graphene.Int(start_date=graphene.Date(), end_date=graphene.Date())

 def resolve_last_created_session(root, info):
        return Session.objects.latest('id')

    def resolve_all_sessions(root, info,first=None,skip=None):
        if skip:
            return gql_optimizer.query(Session.objects.all().order_by('-id')[skip:], info)
        elif first:
            return gql_optimizer.query(Session.objects.all().order_by('-id')[:first], info)
        else:
            return gql_optimizer.query(Session.objects.all().order_by('-id'), info)

(just part of the code, since it's too much and the rest is irrelevant)

Now, as far as I understand to achieve what I want I'd need to have something like a resolve_sessions under the TrainingQuery class, where I can have a paramater user_id that I just pass down from the nested chain. But I don't have a resolver for that field. Sessions is a list of Session, which is a foreign key in the Training model, and this list is brought automatically when I have something like this in a query:


training
  {
    id
    name
    sessions {
      id
      name
    }
  }

I guess the query I want to achieve would be something like this:

query getUser($userId: Int!) {
    getUser(userId: $userId)
    {
        id
        username
        trainings{
            id
            name
            sessions(userId: Int){
                id    
                createdAt
                completedAt
                category
            }
        }
    }
}

but in which place/resolver can I achieve such thing? Is it in the get_queryset method on my SessionType? If so, how?

Am I on the right path here ?


Solution

  • I found the solution. And yes I was on the right track. The problem is really the poor documentation on graphene. I had to open the source code for the ResolveInfo object. You can see it here

    Basically, the parameters passed on the parent level of the query are available under info.variable_values. So all I needed to do was to modify the get_queryset method and do it like this:

    class SessionType(DjangoObjectType):
        class Meta:
            model = Session
            fields = "__all__"
            convert_choices_to_enum = False
    
        @classmethod
        def get_queryset(cls, queryset, info, **kwargs):
            if info.variable_values.get('userId') and info.variable_values.get('userId') is not None:
                return queryset.filter(Q(user_id=info.variable_values.get('userId')))
            return queryset
    

    This is quite an important thing to do, we usually want the filter to work this way. I wish they add this "trick" to their docs. Hopefully this answer will help someone else who stumbles with the same problem