Paginating the results of a Django forms POST request

Viewed 13081

I'm using Django Forms to do a filtered/faceted search via POST, and I would like to Django's paginator class to organize the results. How do I preserve the original request when passing the client between the various pages? In other words, it seems that I lose the POST data as soon as I pass the GET request for another page back to my views. I've seen some recommendations to use AJAX to refresh only the results block of the page, but I'm wondering if there is a Django-native mechanism for doing this.

Thanks.

8 Answers

If you want to access the store data in later request, you would have to store it somewhere. Django provides several ways to archive this:

1) You can use sessions to store the query: Every visitor who visits your site will get an empty session object and you can store whatever you want inside this object, which acts like a dict. Drawback: A single visitor can't do multiple searches with pagination concurrently.

2) Use cookies: If you set a cookie which is stored on the client side, the browser will append the data of the cookie to each request where you can access it. Cookies are more server friendly, because you don't need a session manager for them on the server, but the data stored in cookies is visible (and editable) to the client. Drawback: same as before.

3) Use hidden fields: You can add a form with some hidden fields on your search-result page and store the query inside them. Then, the client will resend the query whenever you submit the form. Drawback: You must use a form with submit buttons for the pagination on your page (simple links wont work).

4) Create Links which contain the query: Instead of using POST, you can also use GET. For example, you could have a link like "/search/hello+world/?order=votes" and "paginated links" like "/search/hello+world/2/?order-votes". Then the query can be easily retrieved from the URL. Drawback: The maximum amount of data you can send via GET is limited (But that shouldn't be a problem for a simple search).

5) Use a combination: You might want to store all the data in a session or a database and access them via a generated key which you can put in the URL. URLs might then look like "/search/029af239ccd23/2" (for the 2nd page) and you can use the key to access a huge amount of data which you have stored before. This eliminates the drawback of solution 1 as well as that of solution 4. New drawback: much work :)

6) Use AJAX: With ajax you can store the data inside some js-variables on the client side, which can then passed to the other requests. And since ajax will only update your result list, the variables aren't getting lost.

I did this in my web application with get parameters Maybe i can help you :

Views.py

class HomeView(ListView):
model = Hotel
template_name = 'index.html'
paginate_by = 10  # if pagination is desired

def get_queryset(self):
   qs = super().get_queryset()
   kwargs = {}
   if 'title' in self.request.GET:
       title = self.request.GET.get('title')
       if title != '':
           kwargs['title__icontains'] = title
   if 'category' in self.request.GET:
       category = self.request.GET.get('category')
       if category:
           kwargs['category_id'] = category
   if 'size' in self.request.GET:
       size = self.request.GET.get('size')
       if size:
           kwargs['size_id'] = size
   if 'service' in self.request.GET:
       service = self.request.GET.get('service')
       if service:
           kwargs['service_id'] = service
   if 'ownership' in self.request.GET:
       ownership = self.request.GET.get('ownership')
       if ownership:
           kwargs['ownership_id'] = ownership
   qs = qs.filter(**kwargs)
   return qs

def get_context_data(self, **kwargs):
    context = super().get_context_data(**kwargs)
    form_init = {}
    form = SearchForm()
    if self.request.GET.items():
        try:
            parameters = self.request.GET.items()
        except KeyError:
            parameters = {}
        for key, value in parameters:
            for field in form.fields:
                if key == field:
                    form_init[key] = value
        form.initial = form_init
    if 'title' in self.request.GET:
       title = self.request.GET.get('title')
       if title != '':
           context.update({
            'title': title
           })
    if 'category' in self.request.GET:
       category = self.request.GET.get('category')
       context.update({
        'category': category
       })
    if 'size' in self.request.GET:
       size = self.request.GET.get('size')
       context.update({
           'size': size
      })
    if 'service' in self.request.GET:
       service = self.request.GET.get('service')
       context.update({
           'service': service
      })
    if 'ownership' in self.request.GET:
       ownership = self.request.GET.get('ownership')
       context.update({
          'ownership': ownership
       })
    context.update({
        'search_form': form
    })
    return context

Pagination file html

<div class="row">
  {% if is_paginated %}
  <nav aria-label="...">
    <ul class="pagination">
      {% if page_obj.has_previous %}
        <li class="page-item"><a class="page-link" href="?category={{category}}&size={{size}}&service={{service}}&ownership={{ownership}}&page={{ page_obj.previous_page_number }}">Previous</a></li>
      {% else %}
        <li class="page-item disabled"><span class="page-link">Previous</span></li>
      {% endif %}
      <span class="page-current">
               Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
           </span>
      {% if page_obj.has_next %}
        <li class="page-item"><a class="page-link" href="?category={{category}}&size={{size}}&service={{service}}&ownership={{ownership}}&page={{ page_obj.next_page_number }}">Next</a></li>
      {% else %}
        <li class="page-item disabled"><span class="page-link">Next</span></li>
      {% endif %}
    </ul>
  </nav>
 {% endif %}
</div>

You can ask request object if it's ajax, simply request.is_ajax. This way you can detect, whether it's first post request or further questions about the next pages.

My suggestion would be to store the post request using a session or a cookie. In case the post data is sensitive, you should use session to store it. The code below contains my logic to implement it using session.

def index(request):
    is_cookie_set = 0
    # Check if the session has already been created. If created, get their values and store it.
    if 'age' in request.session and 'sex' in request.session: 
        age = request.session['age']
        sex = request.session['sex']
        is_cookie_set = 1
    else:
        # Store the data in the session object which can be used later
        request.session['age'] = age
        request.session['sex'] = sex
    if(request.method == 'POST'):
        if(is_cookie_set == 0): # form submission by the user
            form = EmployeeForm(request.POST)
            sex = form.cleaned_data['sex']
            age = form.cleaned_data['age']
            if form.is_valid():
                result = Employee.objects.all(sex=sex,age_gte=age) # filter all employees based on sex and age
        else: # When the session has been created
            result = Employee.objects.all(sex=sex,age_gte=age)
        paginator = Paginator(result, 20) # Show 20 results per page
        page = request.GET.get('page')
        r = paginator.get_page(page)
        response = render(request, 'app/result.html',{'result':result})    
        return response
    else:
        form = EmployeeForm()
    return render(request,'app/home.html',{'form':form})

You should also check if the post fields are empty or not and change the logic according to it. You can also store the whole post request in the session as suggested by @abidibo.

You can also use cookies for the same. I have explained it here

The Below code is working, the first request is a GET request, which accesses the form, will go directly to the else block. Once the user puts up a search query, results will be shown, which will be a post request, and 2nd if block will be activated, this request we will store in a session. When the user accesses 2nd search page, it will be a GET request, but we are checking whether there is an active pagination session, and also checking whether it's not a page request of GET. At this point 1st if block will be trigerred.

def search(request):
    if not request.method == "POST" and 'page' in request.GET:
    if 'search-query' in request.session:
        request.POST = request.session['search-query']
        request.method = 'POST'

    if request.method == 'POST':
    form = Search_form(request.POST)
    request.session['search-query'] = request.POST
    if form.is_valid():
        search_query = form.cleaned_data.get('search_query')
        search_parameter = form.cleaned_data.get('search_parameter')
        print(search_query, search_parameter)
        queryset_list = CompanyRecords.objects.filter(**{f'{search_parameter}__icontains': search_query}).exclude(
            company_name__isnull=True).exclude(description__isnull=True).exclude(phones__isnull=True).exclude(
            emails__isnull=True)[:5]
        page = request.GET.get('page', 1)
        paginator = Paginator(queryset_list, 2)

        try:
            queryset = paginator.page(page)
        except PageNotAnInteger:
            queryset = paginator.page(1)
        except EmptyPage:
            queryset = paginator.page(paginator.num_pages)

        return render(request, 'search/search_results.html', {'queryset': queryset})

    else:
    context = {
        'form': Search_form()
    }
    return render(request, 'search/search.html', context)
Related