Query filter lost with pagination - with Select

Django version 5.1.1
Python 3.12.4

Preface:
I have not found this exact issue discussed (using select input) for model queries and pagination. I have tried many things suggested regarding pagination / query loss issue and no luck. None of it seem to apply exactly. All code displayed is partial (only needed for this forum help request). As usual I’m probably doing many things wrong but this is where / how I learn. Thank you in advance for your support.

Goal:
In a nutshell to not lose model query filter from pagination.

Setup:
index.html page with select input (sel_query) for query selection and include to turnover-list.html page with table heading definition and tbody for loop include to turnover_row.html page with table rows. There is also an include for pagination.html on turnover-list.html page. Just standard partial page load scheme used everywhere.
I also needed to track the very first record loaded at the top of each table page so that record can be loaded in detail in another container (SPA) as each page loads. You will see this code in the views.py code I display. It works well. Now if there is a better way to track this first top of table record(pk) loaded with each page forward / back I’m all ears (not point of this request for help) but always welcome.

Problem:
model.objects.all().filter being lost when using pagination with a twist (query select input) from html page.

views.py (partial)

def turnover_index(request):
    my_datetime = datetime.now()
    new_datetime = my_datetime - timedelta(hours=24)                  # Subtract 24 hours
    if request.POST.get("sel_query", None) is not None:                    
        template = 'turnover_app/turnover-list.html'
        inputtxt = request.POST.get('sel_query')
        if inputtxt == '1': # default completed = False last 24 hrs closed or open      
            tologs = ToLog.objects.all().filter(Q(completed=False) | Q(date_time__gte=new_datetime)).order_by(_ORDBY)   # works good  
        elif inputtxt == '2':     # All Open
            tologs = ToLog.objects.all().filter(Q(completed=False)).order_by(_ORDBY) 
        elif inputtxt == '3':     # all closed
            tologs = ToLog.objects.all().filter(Q(completed=True)).order_by(_ORDBY)   
        elif inputtxt == '4':     # all records
            tologs = ToLog.objects.all().filter().order_by(_ORDBY)             
    else: # default initial page load
        template = 'turnover_app/index.html'
        tologs = ToLog.objects.all().filter(Q(completed=False) | Q(date_time__gte=new_datetime)).order_by(_ORDBY)
            
    tologs_cnt = tologs.count()
    paginator = Paginator(tologs, _ITEMS_PER_PAGE)                      
    page_number = request.GET.get('page')                               
    page_obj = paginator.get_page(page_number)                          
    
    if type(page_number) == str:                                        
        conv_2_num = int(page_number)                                   
        if conv_2_num == 1:                                             
            rec_to_get = 0                                              
        else:                                                           
            rec_to_get = (conv_2_num * _ITEMS_PER_PAGE)                 
            rec_to_get = rec_to_get - _ITEMS_PER_PAGE                   
    else:   # page_number = <class 'NoneType'>                          
        conv_2_num = 1                                                  
        rec_to_get = (conv_2_num * _ITEMS_PER_PAGE)                     
        rec_to_get = rec_to_get - _ITEMS_PER_PAGE                                                                    
                                                                        
    if tologs_cnt == 0:                                                 
        _TOLOG_SRCH_ID_PAGE=1
    else:
        _TOLOG_SRCH_ID_PAGE=(tologs[rec_to_get].id)                                                                                       
    TologPkRecID = _TOLOG_SRCH_ID_PAGE                                                                      
    return render(request, template,{'TologPkRecID': TologPkRecID, 'page_obj': page_obj, 'count': tologs.count()})

index.html (partial)

<div>
            <select class="form-select form-select-sm w-50 mb-3" id ="sel_query" name="sel_query" aria-label=".form-select-sm example"
            hx-post="{% url 'turnover_app:turnover_index' %}" hx-headers='{"X-CSRFToken":"{{ csrf_token }}"}' hx-target="#turnList">
              <option selected>Select Queries</option>
              <option value="1">Default</option>
              <option value="2">All Open</option>
              <option value="3">All Closed</option>
              <option value="4">All Records</option>
            </select>
            </div>
 
<div id="turnList">
          {% include 'turnover_app/turnover-list.html' %}
</div>

pagination.html (complete)

<div class="btn-group" role="group" aria-label="Item pagination">
  {% if page_obj.has_previous %}
      <a href="?page={{ page_obj.previous_page_number }}" class="btn btn-outline-primary">&laquo;</a>
  {% endif %}

  {% for page_number in page_obj.paginator.page_range %}
      {% if page_obj.number == page_number %}
          <button class="btn btn-outline-primary active">
              <span>{{ page_number }} <span class="sr-only">(current)</span></span>
          </button>
      {% else %}
          <a href="?page={{ page_number }}" class="btn btn-outline-primary">
              {{ page_number }}
          </a>
      {% endif %}
  {% endfor %}

  {% if page_obj.has_next %}
      <a href="?page={{ page_obj.next_page_number }}" class="btn btn-outline-primary">&raquo;</a>
  {% endif %}
</div>
<div>Total record count: {{ count }}</div>

The easiest / most direct way to handle this is to render your query variable as an additional query parameter on the url, using request.GET.get to retrieve it in your view. If that variable exists in the url requested, then apply that search parameter to the query in the GET portion of the view.

To elaborate, the new (in Django 5.1) querystring template tag will help you with this. Your pagination links would change from:

<a href="?page={{ page_obj.previous_page_number }}" ...

to:

<a href="{% querystring page=page_obj.previous_page_number %}" ...

Hi philgyford,
I made the change you talked about and still does not work. maybe I’m missing something? I followed your link and could not find related info.
Thank you!

<div class="btn-group" role="group" aria-label="Item pagination">
  {% if page_obj.has_previous %}
      <a href="{% querystring page=page_obj.previous_page_number %}" class="btn btn-outline-primary">&laquo;</a>
  {% endif %}

  {% for page_number in page_obj.paginator.page_range %}
      {% if page_obj.number == page_number %}
          <button class="btn btn-outline-primary active">
              <span>{{ page_number }} <span class="sr-only">(current)</span></span>
          </button>
      {% else %}
          <a href="?page={{ page_number }}" class="btn btn-outline-primary">
              {{ page_number }}
          </a>
      {% endif %}
  {% endfor %}

  {% if page_obj.has_next %}
      <a href="{% querystring page=page_obj.next_page_number %}" class="btn btn-outline-primary">&raquo;</a>
  {% endif %}
</div>
<div>Total record count: {{ count }}</div>

You still need to add your search query parameter to the link that you are creating.

What he is showing is an easier / more “direct” way of adding that parameter to the url.

Ah, I notice that your view suggests your form uses a POST request? Use a GET request for a filter like this. (Use POST for a form that will change something in the database.)

By the way, you haven’t used the querystring tag on the numeric page links yet.

Yes, but the pagination links are GET requests.

Yes, but the querystring tag would make use of the sel_query argument in the URL, which won’t be there if the form is doing a POST.

Or am I missing something and we’re talking at cross purposes?

I believe there are three parts to this.

The form entry is done to initially submit the query. The results are then returned paginated. So once the form has been submitted on the first POST, all the subsequent pages are returned via GET requests for the other pages, using the same query parameters as were submitted with the initial POST.

e.g.

  • Get the initial form, no query active
  • POST the form, get the first page, filtered by the query parameter submitted in the POST
  • Get subsequent pages, using GET, but keeping the original query parameter.

Yes, and so if the initial form was submitted via GET everything else would be simpler and more consistent. And the URL of those filtered results would be cacheable, shareable, bookmarkable, etc.

Maybe there’s a very good reason why the form uses POST, but until I’ve heard it I’m not sure why it does.

Ahhh, got it. Now I see what you were getting at.

1 Like

so I added this to my views.py file turnover_index function to one of the queries and added 'querystring ’ to the return render …that is used in the pagination.html page that’s included.

I get an error:
line 163 in views
error line:
return render(request, template,{‘TologPkRecID’: TologPkRecID, ‘querystring’: querystring.urlencode(), ‘page_obj’: page_obj, ‘count’: tologs.count()})

Partial copy of error:

UnboundLocalError at /turnover_app/turnover_index/
cannot access local variable 'querystring' where it is not associated with a value
Request Method:	GET
Request URL:	http://127.0.0.1:8000/turnover_app/turnover_index/?page=1
Django Version:	5.1.3
Exception Type:	UnboundLocalError
Exception Value:	
cannot access local variable 'querystring' where it is not associated with a value
Exception Location:	C:\Users\EAI24909\OneDrive - Epson America\Desktop\Python Projects\epson\.epsonVE\enoc_proj\turnover_app\views.py, line 163, in turnover_index
Raised during:	turnover_app.views.turnover_index
Python Executable:	C:\Users\EAI24909\.virtualenvs\.epsonVE-iknzfel_\Scripts\python.exe
Python Version:	3.12.4

  elif inputtxt == '3':     # all closed
            tologs = ToLog.objects.all().filter(Q(completed=True)).order_by(_ORDBY) 
            querystring = request.GET.copy()

return render(request, template,{'TologPkRecID': TologPkRecID, 'querystring': querystring.urlencode(), 'page_obj': page_obj, 'count': tologs.count()})

pagination.html (complete)

<div class="btn-group" role="group" aria-label="Item pagination">
  {% if page_obj.has_previous %}
      <a href="{% querystring page=page_obj.previous_page_number %}" class="btn btn-outline-primary">&laquo;</a>
  {% endif %}

  {% for page_number in page_obj.paginator.page_range %}
      {% if page_obj.number == page_number %}
          <button class="btn btn-outline-primary active">
              <span>{{ page_number }} <span class="sr-only">(current)</span></span>
          </button>
      {% else %}
          <a href="?page={{ page_number }}" class="btn btn-outline-primary">
              {{ page_number }}
          </a>
      {% endif %}
  {% endfor %}

  {% if page_obj.has_next %}
      <a href="{% querystring page=page_obj.next_page_number %}" class="btn btn-outline-primary">&raquo;</a>
  {% endif %}
</div>
<div>Total record count: {{ count }}</div>

You don’t need to add all the querystring stuff in your view.

You need to:

  • make the form in your template use the GET method
  • make your view use the GET method instead of POST
  • in your template use the querystring template tag in the numeric pagination link, just like you have with the previous and next links

Hi philgyford,
Thank you so much for your help. I made the changes you prescribed and still not working. It still loses the query on the next page. I think I’m close. I know I must have something wrong.

views.py (partial)

def turnover_index(request, *args, **kwargs):
    
    my_datetime = datetime.now()
    new_datetime = my_datetime - timedelta(hours=24)                  # Subtract 24 hours
    if request.GET.get("sel_query", None) is not None:      
        template = 'turnover_app/turnover-list.html'
        inputtxt = request.GET.get('sel_query')
        if inputtxt == '1': # default completed = False last 24 hrs closed or open
            querystring = request.GET.copy()
            page = querystring.get('page') 
            tologs = ToLog.objects.all().filter(Q(completed=False) | Q(date_time__gte=new_datetime)).order_by(_ORDBY)   # works good  

        elif inputtxt == '2':     # All Open
            querystring = request.GET.copy()
            page = querystring.get('page')
            tologs = ToLog.objects.all().filter(Q(completed=False)).order_by(_ORDBY) 

        elif inputtxt == '3':     # all closed
            querystring = request.GET.copy()
            page = querystring.get('page')
            tologs = ToLog.objects.all().filter(Q(completed=True)).order_by(_ORDBY) 
            
        elif inputtxt == '4':     # all records
            querystring = request.GET.copy()
            page = querystring.get('page')
            tologs = ToLog.objects.all().filter().order_by(_ORDBY) 
              
    else: # default initial page load
        querystring = request.GET.copy()
        page = querystring.get('page')
        template = 'turnover_app/index.html'
        tologs = ToLog.objects.all().filter(Q(completed=False) | Q(date_time__gte=new_datetime)).order_by(_ORDBY)

    tologs_cnt = tologs.count()
    paginator = Paginator(tologs, _ITEMS_PER_PAGE)                      
    page_number = request.GET.get('page', page)                          
    page_obj = paginator.get_page(page_number)                        
    
    if type(page_number) == str:                                        
        conv_2_num = int(page_number)                                   
        if conv_2_num == 1:                                             
            rec_to_get = 0                                             
        else:                                                           
            rec_to_get = (conv_2_num * _ITEMS_PER_PAGE)                 
            rec_to_get = rec_to_get - _ITEMS_PER_PAGE                   
    else:   # page_number = <class 'NoneType'>                          
        conv_2_num = 1                                                  
        rec_to_get = (conv_2_num * _ITEMS_PER_PAGE)                     
        rec_to_get = rec_to_get - _ITEMS_PER_PAGE                       

    if tologs_cnt == 0:                                                 
        _TOLOG_SRCH_ID_PAGE=1
    else:
        _TOLOG_SRCH_ID_PAGE=(tologs[rec_to_get].id)                                                                                     
    TologPkRecID = _TOLOG_SRCH_ID_PAGE                                  
    return render(request, template,{'TologPkRecID': TologPkRecID, 'querystring': querystring.urlencode(), 'page_obj': page_obj, 'count': tologs.count()})
 

index.html (partial)

<div>
            <select class="form-select form-select-sm w-50 mb-3" id ="sel_query" name="sel_query" aria-label=".form-select-sm example"
            hx-get="{% url 'turnover_app:turnover_index' %}" hx-headers='{"X-CSRFToken":"{{ csrf_token }}"}' hx-target="#turnList">
              <option selected>Select Queries</option>
              <option value="1">Default</option>
              <option value="2">All Open</option>
              <option value="3">All Closed</option>
              <option value="4">All Records</option>
            </select>
            </div>

pagination.html (complete)

<div class="btn-group" role="group" aria-label="Item pagination">
  {% if page_obj.has_previous %}
      <a href="{% querystring page=page_obj.previous_page_number %}" class="btn btn-outline-primary">&laquo;</a>
  {% endif %}

  {% for page_number in page_obj.paginator.page_range %}
      {% if page_obj.number == page_number %}
          <button class="btn btn-outline-primary active">
              <span>{{ page_number }} <span class="sr-only">(current)</span></span>
          </button>
      {% else %}
          <a href="?page={{ page_number }}" class="btn btn-outline-primary">
              {{ page_number }}
          </a>
      {% endif %}
  {% endfor %}

  {% if page_obj.has_next %}
      <a href="{% querystring page=page_obj.next_page_number %}" class="btn btn-outline-primary">&raquo;</a>
  {% endif %}
</div>
<div>Total record count: {{ count }}</div>

This is still true - you haven’t done this yet.

Also, you don’t need to pass querystring to the template from your view. So you can remove that from your view, and you don’t need all the lines like querystring = request.GET.copy()

I’m sorry guys. I’m failing as a respectable programmer at age 70. I’m just not getting it. Been programming since 1979 and stuff happens, just more often now. Sorry.

I made the changes as suggested to use get in my index.html page and in the view function. I also made the changes to the pagination.html file adding querystring . I also removed 'querystring ’ from return render(…line. as suggested.

<a href="{% querystring page=page_obj.previous_page_number %}" class="btn btn-outline-primary">&raquo;</a>

<a href="{% querystring page=page_obj.next_page_number %}" class="btn btn-outline-primary">&raquo;</a>

views.py (partial)

def turnover_index(request, *args, **kwargs):
   
    my_datetime = datetime.now()
    new_datetime = my_datetime - timedelta(hours=24)                  # Subtract 24 hours
   
    if request.GET.get("sel_query", None) is not None:                   
        template = 'turnover_app/turnover-list.html'
        inputtxt = request.GET.get('sel_query')
        if inputtxt == '1': # default completed = False last 24 hrs closed or open
            print('1 - Default') 
            tologs = ToLog.objects.all().filter(Q(completed=False) | Q(date_time__gte=new_datetime)).order_by(_ORDBY) 

        elif inputtxt == '2':     # All Open
            print('2 - All Open')
            tologs = ToLog.objects.all().filter(Q(completed=False)).order_by(_ORDBY) 

        elif inputtxt == '3':     # all closed
            print('3 - All Closed')
            tologs = ToLog.objects.all().filter(Q(completed=True)).order_by(_ORDBY) 
            
        elif inputtxt == '4':     # all records
            print('4 - All Records')
            tologs = ToLog.objects.all().filter().order_by(_ORDBY) 
              
    else: # default initial page load
        print('Nothing - Back to Default')
        template = 'turnover_app/index.html'
        tologs = ToLog.objects.all().filter(Q(completed=False) | Q(date_time__gte=new_datetime)).order_by(_ORDBY)
        
   
    tologs_cnt = tologs.count()
    paginator = Paginator(tologs, _ITEMS_PER_PAGE)                     
    page_number = request.GET.get('page')                               
    page_obj = paginator.get_page(page_number)                          
    
    if type(page_number) == str:                                        
        conv_2_num = int(page_number)                                   
        if conv_2_num == 1:                                             
            rec_to_get = 0                                              
        else:                                                           
            rec_to_get = (conv_2_num * _ITEMS_PER_PAGE)                
            rec_to_get = rec_to_get - _ITEMS_PER_PAGE                   
    else:                            
        conv_2_num = 1                                                  
        rec_to_get = (conv_2_num * _ITEMS_PER_PAGE)                     
        rec_to_get = rec_to_get - _ITEMS_PER_PAGE                       

    if tologs_cnt == 0:                                                
        _TOLOG_SRCH_ID_PAGE=1
    else:
        _TOLOG_SRCH_ID_PAGE=(tologs[rec_to_get].id)                     
    TologPkRecID = _TOLOG_SRCH_ID_PAGE                                   
    return render(request, template,{'TologPkRecID': TologPkRecID, 'page_obj': page_obj, 'count': tologs.count()})

pagination.html (complete)

<div class="btn-group" role="group" aria-label="Item pagination">
  {% if page_obj.has_previous %}
      <a href="{% querystring page=page_obj.previous_page_number %}" class="btn btn-outline-primary">&laquo;</a>
  {% endif %}

  {% for page_number in page_obj.paginator.page_range %}
      {% if page_obj.number == page_number %}
          <button class="btn btn-outline-primary active">
              <span>{{ page_number }} <span class="sr-only">(current)</span></span>
          </button>
      {% else %}
          <a href="?page={{ page_number }}" class="btn btn-outline-primary">
              {{ page_number }}
          </a>
      {% endif %}
  {% endfor %}

  {% if page_obj.has_next %}
      <a href="{% querystring page=page_obj.next_page_number %}" class="btn btn-outline-primary">&raquo;</a>
  {% endif %}
</div>
<div>Total record count: {{ count }}</div>

You still need to use the querystring template for this link, which is for the numbered page links:

<a href="?page={{ page_number }}" class="btn btn-outline-primary">

So interesting guys. When I make the change

from

<a href="?page={{ page_number }}" class="btn btn-outline-primary">

to

<a href="{% querystring page=page_number %}" class="btn btn-outline-primary">

in the pagination.html page

as you suggested the query now follows the page as it should but upon selecting the page forward the 1st time it wipes out all the html formatting and pushes it to top left side. Pressing back page does not recover, stays messed up. If I put the original line back in pagination.html all the formatting comes back but the query is once again lost.

1st page screenshot:

next page screenshot:

So try to figure out what’s happening.

If you force refresh the page is it the same?

If you view source in the browser do you see that your stylesheets are linked correctly?

If you view the Network tab of your browser’s developer console, do you see it requesting stylesheets? Do they succeed?

Is this failing page using the same template as ones that succeed?

What’s the difference in the html between the successful and failed pages?

etc