Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Filter ModelChoiceFilter by current user using django-filter

I'm using django-filter which is working great but I am having a problem filtering my drop down list of choices (which is based on a model) by the current user. It's a fairly basic and common scenario where you have a child table which has a many to one relationship to a parent table. I want to filter the table of child records by selecting a parent. This is all fairly easy, standard stuff. The fly in ointment is when the parent records are created by different users and you only want to show the parent records in the drop down list that belongs to the current user.

Here is my code from filters.py

import django_filters
from django import forms
from .models import Project, Task
from django_currentuser.middleware import get_current_user, get_current_authenticated_user

class MasterListFilter(django_filters.FilterSet):
    project = django_filters.ModelChoiceFilter(
        label='Projects',
        name='project_fkey',
        queryset=Project.objects.filter(deleted__isnull=True, user_fkey=3).distinct('code')
        )

    class Meta:
        model = Task
        fields = ['project']

    @property
    def qs(self):
        parent = super(MasterListFilter, self).qs
        user = get_current_user()        
        return parent.filter(master=True, deleted__isnull=True, user_fkey=user.id) 

This bit works fine:

@property
    def qs(self):
        parent = super(MasterListFilter, self).qs
        user = get_current_user()        
        return parent.filter(master=True, deleted__isnull=True, user_fkey=user.id) 

This filters my main list so that only records that have a master flag set, have not been deleted and belong to the current user are shown. This is exactly what I want.

This following bit also works and gives me the filtered drop down list that I am looking for because I have hardcoded 3 as the user.id

queryset=Project.objects.filter(deleted__isnull=True, user_fkey=3).distinct('code'),

Obviously I don't want to have a hardcoded id. I need to get the value of the current user. Following the same logic used for filtering the main table I end up with this.

class MasterListFilter(django_filters.FilterSet):
    **user = get_current_user()**
    project = django_filters.ModelChoiceFilter(
        label='Projects',
        name='project_fkey',
        queryset=Project.objects.filter(deleted__isnull=True, user_fkey=**user.id**).distinct('code')
        )

However this is unreliable as sometimes it shows the correct list and sometimes it doesn't. For example if I login and it's not showing the list (ie it shows just '---------') and then I restart my apache2 service, it starts to work again, then at some point it drops out again. Clearly this is not a long term solution.

So how do I reliably get the current user into my filter.py so that I can use it to filter my drop down filter list.

Thanks in advance and happy coding.

EDIT: So following Wiesion's suggestion I changed my code as suggested but I still get a None Type Error saying that user has no attribute ID. BAsically it seems I'm not getting the current user. So going back to the docs and trying to merge their suggestion with Wiesion (whose explanation makes total sense - Thanks Wiesion) I came up with the following:

def Projects(request):
    if request is None:
        return Project.objects.none()
    return lambda req: Project.objects.filter(deleted__isnull=True, user_fkey=req.user.id)

class MasterListFilter(django_filters.FilterSet):
    project = django_filters.ModelChoiceFilter(
        label='Projects',
        name='project_fkey',
        queryset=Projects
        )

    class Meta:
        model = Task
        fields = ['project']

This kind of works in theory but gives me nothing in the drop down list because

 if request is None:

is returning True and therefore giving me an empty list.

So...can anyone see where I'm going wrong which is preventing me from accessing the request? Clearly the second portion of code is working based on qs that is passed from my view so maybe I need to pass in something else too? My view.py code is below:

def masterlist(request, page='0'):
    #Check to see if we have clicked a button inside the form
    if request.method == 'POST':
        return redirect ('tasks:tasklist')
    else:
        # Pre-filtering of user and Master = True etc is done in the MasterListFilter in filters.py
        # Then we compile the list for Filtering by. 
        f = MasterListFilter(request.GET, queryset=Task.objects.all())
        # Then we apply the complete list to the table, configure it and then render it.
        mastertable = MasterTable(f.qs)
        if int(page) > 0:
            RequestConfig(request, paginate={'page': page, 'per_page': 10}).configure(mastertable) 
        else:
            RequestConfig(request, paginate={'page': 1, 'per_page': 10}).configure(mastertable)  
        return render (request,'tasks/masterlist.html',{'mastertable': mastertable, 'filter': f}) 

Thanks.

like image 757
cander Avatar asked Oct 15 '25 18:10

cander


2 Answers

As stated in the following thread, you have to pass the request to the filter instance in the view: Customize queryset in django-filter ModelChoiceFilter (select) and ModelMultipleChoiceFilter (multi-select) menus based on request

ex:

myFilter = ReportFilter(request.GET, request=request, queryset=reports)
like image 84
Louis George Avatar answered Oct 19 '25 11:10

Louis George


From the docs

The queryset argument also supports callable behavior. If a callable is passed, it will be invoked with Filterset.request as its only argument. This allows you to easily filter by properties on the request object without having to override the FilterSet.__init__.

This is not tested at all, but i think something along these lines this is what you need:

class MasterListFilter(django_filters.FilterSet):
    project = django_filters.ModelChoiceFilter(
        label='Projects',
        name='project_fkey',
        queryset=lambda req: Project.objects.filter(
            deleted__isnull=True, user_fkey=req.user.id).distinct('code'),
    )

    class Meta:
        model = Task
        fields = ['project']

Also if it's depending from webserver restarts - did you check caching issues? (In case, django-debug-toolbar gives great insights about that)

EDIT

The unpredictable behaviour most probably happens because you are retrieving the user within the class MasterListFilter definition, so get_current_user() is executed at class loading time, not during an actual request and all subsequent calls to qs will retrieve that query. Generally everything request-related should never be in a class definition, but in a method/lambda. So a lambda which receives the request argument and creates the query only then should exactly cover what you need.

EDIT 2

Regarding your edit, the following code has some issues:

def Projects(request):
    if request is None:
        return Project.objects.none()
    return lambda req: Project.objects.filter(deleted__isnull=True, user_fkey=req.user.id)

This either returns an empty object manager, or a callable - but the method Project itself is already a callable, so your ModelChoiceFilter will receive only an object manager when the request object is None, otherwise a lambda, but it is expecting to receive an object manager - it can't iterate over a lambda so it should give you some is not iterable error. So basically you could try:

def project_qs(request):
    # you could add some logging here to see what the arguments look like
    if not request or not 'user' in request:
        return Project.objects.none()
    return Project.objects.filter(deleted__isnull=True, user_fkey=request.user.id)

# ...
queryset=project_qs
# ...
like image 27
wiesion Avatar answered Oct 19 '25 13:10

wiesion



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!