Additionally filtering Django map by other criteria with viewsets.py if found in GET value

Asked over at SO, I’d like to further filter the map made from @pauloxnet 's great tutorial by GET values if they’re found, but I don’t know how to do this with viewsets.py / fancy API stuff I don’t understand. Please see SO question for full context.

views.py makes a lot more sense to me and I’ve already done this with another part of my project. Would this be better accomplished by trying to convert this viewsets.py class into something for views.py? Would that even be possible with what we’re doing with api.py and serializers.py?

Also, I’m unsure if this should be asked here or over in some Django Rest Framework forum, so please feel free to tell me to go ask elsewhere!

There are a number of people here with various degrees of knowledge of DRF who are certainly willing to try and help.

ViewSets are documented at Viewsets - Django REST framework. Your viewset class implements one or more of the defined methods - each of which can create querysets with whatever parameters are available.

(It would be a lot easier to explain if we were talking in the context of some actual code. The examples on the referenced docs do show the basic ideas.)

1 Like

OK cool I’ll share some code and elaborate on what I supplied in the SO question. My (@pauloxnet 's) viewsets.py:

class MarkerViewSet(viewsets.ReadOnlyModelViewSet):
    """Marker view set."""

    bbox_filter_field = "geom"
    filter_backends = (filters.InBBoxFilter,)
    queryset = Tablename.objects.all()
    serializer_class = MarkerSerializer
    pagination_class = GeoPaginator

This works nicely despite me not really understanding it. The response entries are what are found within the bbox supplied in the url we defined in map.js: "const markers_url = /api/markers/?in_bbox=${map.getBounds().toBBoxString()}", which comes in like “/api/markers/?in_bbox=149.7503471374512,-36.96112442377135,150.1019096374512,-36.90486603587978”. Side note: this is far too many decimals but I’ll figure that out later. All works beautifully.

I’d like to expand on this functionality to also filter by other criteria if they’re found in GET values, but I don’t know how. I’ve already done this for another part of my project with views.py and can easily expand upon this by adding more qs = qs.filter():

def EgFilterView(request):
    qs = Tablename.objects.all()
    date_min_query = request.GET.get('dmin')

    min_date = 1970

    qs = qs.filter(date__gte=dt.datetime(int(date_min_query), 1, 1, tzinfo=pytz.UTC))

    context = {
        'queryset': qs,
        'dmin': min_date,
    }

    return render(request, "main_page.html", context)

I’d like to expand on what viewsets.py is doing with similar ease but I don’t know how. So for example if the dmin GET value contains an int corresponding to a year I’d like to filter results to be everything in the bbox and occurring after dmin. I think I might be able to by adding to filter_backends = (filters.InBBoxFilter,), but I don’t know where to start.

My interpretation of viewsets.py documentation (ty for the link) is that viewsets.py accomplishes the same as views.py in a more advanced and elegant way but I’m not at that level yet. I’m thinking it might be best to try converting all of this to views.py but I don’t know if that’s even possible. Maybe I’m missing something?

Sorry for the silly questions. Thank you so much for your patience and help!

Nope, not silly and you are really close.

I’m not familiar with the rest_framework_gis filters, but I wouldn’t believe they would be required in this case.

In your view you have:

You’ve got a queryset (qs), that you are modifying in the last line (qs = qs.filter(...))

In your viewset, you have a queryset:

That you then want to modify, in the same manner as you did in your view. (queryset = queryset.filter(...))

It really should be that simple. (The only caveat would be if there’s some unusual and unexpected interaction between the filters package being used and standard queryset construction - which I highly doubt)

1 Like

Hah, wow! That really is simple once you see it. I feel like an idiot :laughing:

I’ve added queryset = queryset.filter(date__gte=dt.datetime(2020, 1, 1, tzinfo=pytz.UTC)) and that’s worked nicely!

There’s a slight complication though: I don’t know how to supply the GET values and that only worked cause I manually supplied 2020. In views.py I’ve got the request coming in as EgFilterView(request) but I can’t seem to supply request as an argument of MarkerViewSet(). I also don’t understand how viewsets.py seems to have got the bbox values with filter_backends = (filters.InBBoxFilter,), so dunno how I might be able to tweak that.

The request URL seems built in map.js: "const markers_url = /api/markers/?in_bbox=${map.getBounds().toBBoxString()}", so the URL itself doesn’t include the GET values and it seems it might need to. Is there a good way to make JS include all other GET values? I’ve had a search and couldn’t find anything.

Thanks again,

Something I didn’t pay too much attention to at first is that you’re working with a Class Based View, which works a bit differently than a function-based view.

If you’re not used to working with them, see Class-based views | Django documentation | Django and Introduction to class-based views | Django documentation | Django.
Then continue with the DRF-specific documentation at Views - Django REST framework and Generic views - Django REST framework. (Yes, it’s a lot of reading. Understanding ClassBased Views does take time and effort.)

The Reader’s Digest version is that you actually modify the functionality of a CBV by overriding functions within the class.

For example, the ReadOnlyModelViewSet class has a get_queryset method that is what you override to change the queryset on a per-call basis.

Changing the class-based definition as you have here sets the queryset one time - it doesn’t change it for every call. See the docs at Viewsets - Django REST framework for an example of a viewset with a get_queryset method.

I know that wrapping your head around class-based views can be difficult if you’re not comfortable with class inheritance. In this case, since you’re working with DRF, I’d recommend you become familiar with the Classy Django REST Framework site.

I’m sorry, I’m not following you at all here. Can you clarify what you’re trying to ask?

Aha you’ve got me very confused now :sweat_smile:

I’m trying to supply the GET values from other inputs on the page eg <input type="hidden" name="test", id="test"/>. They’re supplied with request in views.py:

def EgFilterView(request):
    ...
    date_min_query = request.GET.get('dmin')

But in viewsets.py class MarkerViewSet(viewsets.ReadOnlyModelViewSet) throws a name error when supplied MarkerViewSet(...,request) and I dunno how else to supply it.

Complicating this, the request seems driven by the code in map.js: "const markers_url = /api/markers/?in_bbox=${map.getBounds().toBBoxString()}". I’d like to figure out how to expand on this so that the request also includes all GET values so they can be used when request is hopefully supplied to class MarkerViewSet.

I understand what you’re trying to do. However, you can’t do what you want until you start to use the class-based views properly.

First step - create a get_queryset method and move all the code related to making the queryset for this class into it.

Then, either review the previously-referenced docs at Generic views - Django REST framework to see how to access the request object within an overridden method, or review the setup method in the Classy DRF docs page for the ReadOnlyModelViewSet to see where the request is stored in the class.

This line of code you’re referencing - is this code in a file you’ve created, or is it part of some 3rd party library?
At the time that this code is going to execute, how are you going to know what values to supply? Are there other widgets on the page from which you are going to retrieve values?
How / when are you issuing the GET?
(I think we’re going to need to see more of the JS to be able to address this.)

(I think we’re going to need to see more of the JS to be able to address this.)

OK, cool! This JS comes from this section of @pauloxnet 's tutorial.

At the time that this code is going to execute, how are you going to know what values to supply? Are there other widgets on the page from which you are going to retrieve values?

Yes, there are several other inputs on the page which may be populated with GET values that I’d like the query to filter for if the value is supplied.

Simply adding a new input like <input type="hidden" name="test", id="test"/> automatically populates the regular request urls with the GET parameter &test=, but as the map request is explicitly defined as "const markers_url = /api/markers/?in_bbox=${map.getBounds().toBBoxString()} " I don’t know how to include this input value in the URL. Maybe there’s some JS function which can create a string of all the inputs and values to include in markers_url?

First step - create a get_queryset method and move all the code related to making the queryset for this class into it.

I’m kinda lost now… I haven’t worked with methods yet and still don’t fully understand classes. I’ve tried what seemed the simplest step of that by adding def_queryset():

class MarkerViewSet(viewsets.ReadOnlyModelViewSet):
    ...
    @classmethod
    def get_queryset(self):
        print(self.request)
    ...

But this breaks it… am I going about this the wrong way? Should I include this @classmethod? Do you mean move the entirety of the class code into this new method?

Thanks again for your patience!

You move only the code that constructs the queryset into this method.

And no, you don’t use the classmethod decorator.

1 Like

You’re just building a string. You can concatenate whatever values desired on to this string. How you want to do this depends both upon whatever other JavaScript libraries you’re using and specifically how you’re adding these values on the page.

1 Like

OK, after a few hours (new to JS) I’ve figured out how to build the URL!

const form = document.querySelector('#theForm');
var object = Object.values(form).reduce((obj,field) => { obj[field.name] = field.value; return obj }, {});  // https://stackoverflow.com/a/47188324
delete object[""];
const markers_url = `/api/markers/?in_bbox=${map.getBounds().toBBoxString()}` + '&' + new URLSearchParams(object).toString()

Now only need to feed this URL in and we’re there! I’ll start trying to figure out this method…

OK, I’ve had a good play around with this and have somehow sorta got it with the help of this SO answer. Moving the code that constructs the queryset into the method (without extra filter discussed above):

class MarkerViewSet(viewsets.ReadOnlyModelViewSet):
    """Marker view set."""
    bbox_filter_field = "geom"
    filter_backends = (filters.InBBoxFilter,)
    queryset = Tablename.objects.all()

    def get_queryset(self):
        bbox = self.request.query_params.get('in_bbox')
        print(bbox)
        queryset = Tablename.objects.all()
        return queryset

    serializer_class = MarkerSerializer
    pagination_class = GeoPaginator

This prints bbox which is the GET param we want! So I can do this for other params and use those values to filter queryset as discussed above and we’re in business!

Only issue is queryset = Tablename.objects.all() and filtering I’ve removed for eg simplicity is duplicated and if I remove either I get “AssertionError: basename argument not specified, and could not automatically determine the name from the viewset, as it does not have a .queryset attribute” or “AttributeError: ‘NoneType’ object has no attribute ‘filter’”.

So now I don’t understand where the filtering is done or how to remove this duplicated code. Is there something I’ve missed on a more fundamental level?

Ok, we’re really getting out of the areas where I have any real knowledge. A cursory scan of the docs at Filtering - Django REST framework lead me to believe you can replace filter_backends with filterset_class to use that filter in the CBV. (You might need to start reading from earlier in that page - perhaps from the top - for this to make sense.)
But this is all conjecture based upon what I’m reading and not on any specific knowledge or experience.

So now I don’t understand where the filtering is done or how to remove this duplicated code. Is there something I’ve missed on a more fundamental level?

The filtering happens inside the filter_queryset method. The drf code is very opinionated and object oriented. The ReadOnlyModelViewSet is made up of two mixins. When you’re making a list request, it’s being handled by the ListModelMixin's list method.

To extend the filtering behavior that already exists on the viewset, you can extend the filter_queryset method

def filter_queryset(self, queryset):
   queryset = super().filter_queryset(queryset)
   extra_field1 = self.request.query_params.get('extra_field1')
   if extra_field1:
      queryset = queryset.filter(extra_field1=extra_field1)
   return queryset

If you haven’t done so yet, take the time to go through the django rest framework tutorial

1 Like

This worked perfectly! Thank you so much!

I don’t fully understand class based views / what “object oriented” is but I now see how defining filter_queryset() overrides the default method.

I also updated my SO thread. Please feel free to correct me if I’ve explained anything inaccurately in my answer.

Thanks again fellas!

1 Like

You’re welcome! And your summary in the stack overflow post is great.

1 Like