Reusable Queries with Q

G’day all,

I’m trying to be a little clever and make my querysets more reusable. I have a long list of endpoints where I have objects related to either a case or a visit. In the example below, I demonstrate a queryset which returns objects of a particular model as defined in a view. The purpose of this queryset is to limit the objects in the queryset based on the access to the underlying objects that a user should or should not have.

def visit_related_queryset(view):
    """
    We want to find all the objects which are related to the user
    by way of `case` and the user's relation to it. e.g., did the user
    create the case, are they a contributor or are they a uni admin
    and should therefore see all of a uni's objects + any other objects
    belonging to a case to which they have contributed.
    :param view: The DRF view
    :return: A queryset of the object defined in the view
    """
    user = view.request.user
    uni = Q(visit__case__contributing_uni=user.university)
    contributor = Q(visit__case__contributors__contributor__user=user)
    case_creator = Q(visit__case__creator__user=user)
    
    permitted_users = Q(contributor | case_creator)  # for regular users
    admin = Q(contributor | case_creator | uni)  # for uni admin staff
    
    visit = Q(visit__id=view.kwargs["visit"])

    if is_eclinic_admin(user):
        return view.model.objects.all()
    if is_uni_admin(user):
        return view.model.objects.all().filter(visit, admin)
    return view.model.objects.all().filter(visit, permitted_users)

This queryset works well, and after moving to it from similarly custom querysets under each view where the visit’s UUID is used as the filter, all tests continue to pass. That’s a relief!

In addition to the visit related views, I have many views that don’t directly related to a visit, but do so indirectly via a FK relationship.

As an example, a question object has this relationship to Visit and Case:
quiz__visit__case

and an answer object this relationship to Visit and Case:
question__quiz__visit__case

I’m trying to find a way whereby I can reuse my def visit_related_queryset(view) method to filter queries for endpoints which do not have a direct relationship to Visit. At first I thought I could use **kwargs but I came unstuck when trying to workout how I would unpack **kwargs into the correct Q filters.

Additionally, I’m trying to work out how I can pass a string as an argument kwarg, e.g., I was thinking I could pass a variable into Q to achieve something like this: `Q(my_filter=my_value). This could be as much as a lack of Python specific knowledge as it is a lack of Django specific knowledge.

I’m playing around with the above two thoughts trying to find an elegant solution and avoiding have to write custom queryset methods with only very minor filtering variations. I thought it a good idea to raise my hand here whilst playing around and before I get myself in a pickle.

edit
I should mention what I am currently doing to make my code a bit more reusable. I have first made the following queryset:

def get_visit_relation_queryset(view, uni, contributor, creator):
    user = view.request.user
    permitted_users = Q(contributor | creator)
    admin_staff = Q(contributor | creator | uni)
    visit = Q(visit__id=view.kwargs["visit"])

    if is_eclinic_admin(user):
        return view.model.objects.all()
    if is_uni_admin(user):
        return view.model.objects.all().filter(visit, admin_staff)
    return view.model.objects.all().filter(visit, permitted_users)

and then depending on the view, I do something like this:

    def get_queryset(self):
        user = self.request.user
        uni = Q(history__visit__case__contributing_uni=user.university)
        contributor = Q(history__visit__case__contributors__contributor__user=user)
        creator = Q(history__visit__case__creator__user=user)
        return querysets.get_visit_relation_queryset(self, uni, contributor, creator).select_related("visit")

As always folks, thank you for your input and help.

Cheers,

Conor

Sorry, don’t have the time at the moment to really digest your message, but there was one tiny part I can comment on right now:

The “**kwargs” mechanic passes a dictionary in as a set of keyword args and values.

So for example:

User.objects.filter(first_name='Ken', last_name='Whitesell')

is exactly the same as:

kwargs = {'first_name': 'Ken', 'last_name': 'Whitesell'}
User.objects.filter(**kwargs)

or even:
User.objects.filter(**{'first_name': 'Ken', 'last_name': 'Whitesell'})

Keeping that equivalence in mind allows you to dynamically create any query or set of Q objects to build arbitrary queries on the fly.

3 Likes

Thank you, Ken. No need to apologise mate, you’re literally the most helpful and supportive person on this forum!

1 Like