Basic Django Search Queries with Pagination

Previously on {{ PLP }}, we have gained more knowledge and experienced on the Basic Django CRUD using AJAX, JSON, and JQuery – Monolithic Architecture in which we have a thorough discussions on the essence of Django database operations and rest assured, if you understand the Django CRUD, the rest of our future discussions in relation with the database activities definitely would be easy for you to understand as well.

In this event loop, is the continuous discussion about the foundation of Django search database table row capabilities using two methods such as the native Django search form submission for better SEO (Search Engine Optimization) that’s an important for a public page to have such kind of a search ability that the user might save the typed-in keywords from a search box and save it as a bookmark from their web browsers.

However, using AJAX, JQuery, and JSON mainly useful for non-public pages. What does this mean?, these pages are non-index-able by search engines, why? because, these pages usually have user authentications such as the username and password and no point for the search engines to access these pages.

Introduction

Though, I’ll be splitting the Django search capabilities into two methods which are totally separate in nature, usage, and the benefits of it for your Django project.

So, prepare your self once again and stay focus because this would be exciting and fun learning with {{ PLP }}.

Getting Started

Similarly, I’ll be discussing in more detailed information with the actual page example for you to test out on how exactly it behaves with the two most important Django search capabilities that are very useful for our Django projects. So, let’s get started.

In addition, we’re using the same MySQL database (dev_auth) and table name dev_contact_us from our previous actual example called Django Basic CRUD: Manage Table Rows using DataTables for AJAX, JQuery, and JSON for Django search non-public pages.

But, we’ll be creating a completely new public HTML page with some basic SEO compliance for the native Django search functionality.

Django Search using AJAX, JQuery, and JSON Method

Meanwhile, in this method, we’ll be using the previous actual example page and continue to do the coding part using AJAX, JQuery, and JSON format as well. So, let’s follow the step-by-step guide just for you.

Step 1: Import the following built-in Django and Python libraries

Now, open the dev/myroot/views/vmain.py and import these 3 built-in libraries provided by Django and Python by itself.

1
2
3
from django.utils.text import slugify
from django.db.models import Q
import json

The import json is a built-in Python library that the main role is to encode and decode any strings into JSON format, but in our case, is the Django QuerySet results that we need to convert it into a JSON format to be passed on through the AJAX request after the successful search events and then automatically display it to the DataTables object without refreshing the entire HTML page.

Step 2: Create a new View for the Search Text event

Next, open the same dev/myroot/views/vmain.py and insert this code snippet below at the bottom line.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def basic_search_text_view(request):
    """Renders the basic search text."""
    if request.method == "POST":
        fsearch = request.POST.get('fsearch')

        # Filter data by using __icontains built-in Django function
        data_lists = ContactUs.objects.filter(
                        Q(is_deleted=False) &
                        (Q(full_name__icontains=fsearch) |
                        Q(email__icontains=fsearch) |
                        Q(subject__icontains=fsearch) |
                        Q(message__icontains=fsearch))).order_by('-id')[:50]

        fh_data = dict()
        fh_list = []

        for fh in data_lists:
            url = settings.BASE_URL + slugify(fh.full_name) + "-" + str(fh.id)
            trun_subject = fh.subject[:100] + '...'

            # Convert UTC datetime from db to user's local datetime.
            submitted_date = convert_to_local_datetime(fh.submitted)

            edit_url = settings.BASE_URL + "basic_crud/" + str(fh.id) + "/change/"

            fh_list.append(
                    {'full_name': (fh.full_name),
                     'subject': trun_subject,
                     'email': fh.email,
                     'submitted': submitted_date,
                     'id': fh.id,
                     'url': url,
                     'edit_url': edit_url
                     })

        fh_data = fh_list
        json_data = json.dumps(fh_data)
        return JsonResponse(json_data, safe=False)

The Django built-in Q objects is a collection of keywords for Django QuerySet that we can combine the operators such as the AND and OR statement.

1
2
3
4
5
6
7
# Filter data by using __icontains built-in Django function
data_lists = ContactUs.objects.filter(
                Q(is_deleted=False) &
                (Q(full_name__icontains=fsearch) |
                Q(email__icontains=fsearch) |
                Q(subject__icontains=fsearch) |
                Q(message__icontains=fsearch))).order_by('-id')[:50]

The Django query using the Q objects with the __icontains would generate the actual MySQL query like this.

1
SELECT * FROM dev_contact_us WHERE is_deleted = 0 AND (full_name LIKE '%mm%' OR email LIKE '%mm%' OR subject LIKE '%mm%' OR message LIKE '%mm%') ORDER BY id DESC LIMIT 50

Thus, Django QuerySet will NOT execute the SQL query unless you iterate it using for loop or things alike.

As a result, the Django QuerySet must be iterate using for loop and store each rows using the Python list [] in which we initialize it on top of the for loop statement first, like fh_list = [] as our example.

There is no straightforward approach before we can use the JSON formatted QuerySet results, because as I’ve mentioned earlier, we use the AJAX method to automatically reload the DataTables object or even any HTML elements like <div>, <table>, etc without refreshing the entire HTML page.

How to store the Iterated Django QuerySet to Python lists?

Perhaps, we need to use the lists.append() to store each rows into the Python lists object, in our case will be further process below.

1
2
3
4
5
6
7
8
9
fh_list.append(
    {'full_name': (fh.full_name),
     'subject': trun_subject,
     'email': fh.email,
     'submitted': submitted_date,
     'id': fh.id,
     'url': url,
     'edit_url': edit_url
     })

Above, you can add your custom column names as you need it, like for example the url column while we concatenate few fields like the full_name and the id, by the way, we use the slugify which is the Django built-in library to make each white spaces converted automatically into a “hyphen symbol.

1
url = settings.BASE_URL + slugify(fh.full_name) + "-" + str(fh.id)

Moreover, to omit the characters from a string, we can use the Python built-in function which is the string_variable[:length] for example fh.subject[:100] + ‘…’, I added the triple dots as an optional to illustrate that the long strings have been omitted.

In the same way, outside the for loop scope, we need to store from the Python lists [] to Python dictionary dict() object before the json.dumps() assignment. Finally, issue the command to return JsonResponse(json_data, safe=False), to serialize the JSON objects and ensure that the safe=False must be set, otherwise it will throw a TypeError from the Python’s interpreter.

Step 3: Create a new custom Function to Automatically adopt the local user’s timezone.

In the meantime, we need to convert the stored database table row’s date and time value from UTC to the user’s localize timezone. By default, Django uses the UTC timezone as you can check it from the dev/dev/settings.py.

1
2
3
4
5
6
7
8
# Internationalization
# https://docs.djangoproject.com/en/2.1/topics/i18n/

LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True

Yet, this technique is a good practice when we’re developing a Django powered site to store the UTC date and time value to the database if you’re site is open for global users and automatically converts the UTC with their own timing.

First, install the timeago Python library

Hence, access your web console / terminal with root privileges so we can install. Then, execute the command below.

1
pip3 install timeago

Remember, include this timeago Python library to your dev/requirements.txt file so we can keep track of the dependencies we have, and actually, we can install it in just one command line all of the dependencies listed from the requirements.txt file.

Next, open your dev/myroot/views/vfunctions.py and import the following Python libraries including the newly installed timeago.

1
2
from datetime import datetime, timedelta
import timeago

Furthermore, create a new function with the following code snippets below.

1
2
3
4
5
6
7
8
9
10
11
def convert_to_local_datetime(dt_value):
    MY_DT_FORMAT = '%Y-%m-%d %H:%M:%S'
    dt = dt_value
    created_date_str = dt.strftime(MY_DT_FORMAT)  # convert to string
    created_date = dt.strptime(created_date_str, MY_DT_FORMAT)

    # timeago in python library to get user's local time
    now = datetime.now() + timedelta(seconds=60 * 3.4)
    event_date = timeago.format(created_date, now)

    return str(event_date)

Next, open the dev/myroot/views/vmain.py and import our customize Python function.

1
from myroot.views.vfunctions import dict_alert_msg, convert_to_local_datetime

Earlier, we have imported the dic_alert_msg custom function that we’ve made previously, just add the convert_to_local_datetime separated with a comma and one space in between before we can use this custom function.

Add new URL from the URLS.py

Furthermore, add a new URL from the dev/dev/urls.py when triggering the AJAX search text event.

1
2
path('basic_search_text/', myroot.views.vmain.basic_search_text_view,
         name='basic_search_text'),

Subsequently, this basic_search_text URL will be used for our AJAX event later on.

Modify the basic_crud_list.html

Moreover, modify the dev/myroot/templates/myroot/basic_crud_list.html and add the following AJAX and JQuery event inside the $(document).ready(function().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//Hit enter key from input search box
$('#fsearch').on("keypress", function(e) {
    if (e.keyCode == 13) {
    $('button[id = btnSearchFile]').click();
        return false;
    }
});

//Search data by text
$('.SearchDataText').on("click",function()
{
  //GET THE VALUE
    var csrfmiddlewaretoken = Cookies.get('csrftoken');

  $("#divSearchFile").find("input").serialize();
    fsearch = $("#fsearch").val().trim();

    if (csrfmiddlewaretoken === undefined || csrfmiddlewaretoken === null || csrfmiddlewaretoken === ""){
        swal("CSRF Token is Missing!", "Please refresh this page and try again.", "error");
        return false;
    }

    $.ajax
  ({
    type: "POST",
    url: BASE_URL+"basic_search_text/",

    <!--INSERT PARAMETERS HERE-->
    data: 'csrfmiddlewaretoken='+csrfmiddlewaretoken +'&fsearch='+fsearch,
    cache: false,
    dataType: "json",
    success: function(jResults)
    {
            $('#tbData').empty();
            var table = $('#tblData').DataTable();
            table.clear();

            var rowStatus = '';
      var data = jQuery.parseJSON(jResults);
      $.each(data, function(i, obj) {

                var add_row = `<a href="{% url 'basic_crud_create' %}"><i class="fa fa-plus"></i></a>`;
                var edit_row = `<a href="`+ obj.edit_url +`"><i class="fa fa-edit"></i></a>`;
                var del_row = `<a href="javascript:void(0);" onclick="confirmUserDeleteAction(this);" row-id="`+ obj.id +`"><i class="far fa-trash-alt"></i></a>`;
                var visit_link = `<a href="`+ obj.url +`" target="_blank"><i class="fa fa-globe"></i></a>`;

                table.row.add([
                    obj.full_name,
                    obj.subject,
                    obj.email,
                    obj.submitted,
                    add_row,
                    edit_row,
                    del_row,
                    visit_link,
                    obj.id
                ]).node().id = 'tr'+obj.id;
                table.draw( false );
          });
    }
  });
});

Likewise, whatever JavaScript codes you have inside the
$(document).ready(function() will run once the HTML page Document Object Model (DOM) will be fully ready in its state.

1
2
3
4
5
6
7
8
9
10
11
12
<div class="col-2 d-none d-sm-block">
    <div class="form-group pl-2 mb-0 mt-3 pr-2 float-right">
        <div class="input-group" id="divSearchFile">
            <input type="text" class="form-control" id="fsearch" name="fsearch" placeholder="Search Records">
            <div class="input-group-append">
                <button id="btnSearchFile" class="btn btn-secondary SearchDataText">
                    <i class="fa fa-search"></i>
                </button>
            </div>
        </div>
    </div>
</div>

Thus, adding a button class that binds with this SearchDataText JS function when the button click event triggered by the user.

Upload all the new and modified files to your web server.

Finally, upload all the changes we have made to your own web server using FileZilla. Afterward, reload your daphne web service to see the changes we’ve done.

1
daphne -b 172.104.190.249 -p 8000 dev.asgi:application

Now, for our live demo about this changes, open the Django Basic CRUD: Manage Table Rows using DataTables page and try searching any text and see how it behaves and surely when you hit that search button or after typing the keyword of your choice then you press ENTER key, it will NOT reload the entire page, instead, it automatically redraws the DataTables object each row and load the JSON results.

Another Example of Django Search filtered by a Date Range using DateRangePicker object

For this purpose, I’m using the open source DateRangePicker to select date criteria when we’re filtering the data, this is important for any application to allow the user to filter the data using this option.

Step 4: Import the following Python libraries

Once again, open the dev/myroot/views/vmain.py and import the following 2 Python built-in libraries.

1
2
import pytz
from datetime import datetime, timedelta

Hence, the main role of the pytz Python library is to solve various cross timezone platform and it calculates accurately based on our Django’s default UTC timezone where we store it from a database table and interprets it to the user’s current timezone.

1
pip3 install pytz

Just in case you don’t have the pytz installed in your UBUNTU web server, you may execute the command above to install it.

Step 5: Create a new View for the Search Date Range event

Next, open the same dev/myroot/views/vmain.py and insert this code snippet below at the bottom line.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def basic_search_dr_view(request):
    """Renders the basic search by date and time."""
    if request.method == "POST":

        # Get the date range values from the user input
        mStartDate = request.POST.get('mStartDate')
        mEndDate = request.POST.get('mEndDate')

        # Format date
        date_format = '%Y-%m-%d'

        unaware_start_date = datetime.strptime(mStartDate, date_format)
        aware_start_date = pytz.utc.localize(unaware_start_date)

        unaware_end_date = datetime.strptime(mEndDate, date_format
                                             ) + timedelta(days=1)
        aware_end_date = pytz.utc.localize(unaware_end_date)

       # Display data, using __range from Django's built-in functionality
        data_lists = ContactUs.objects.filter(
                            is_deleted=False,
                            submitted__range=(aware_start_date,
                                                 aware_end_date)
                            ).order_by('-id')[:50]

        fh_data = dict()
        fh_list = []

        for fh in data_lists:
            url = settings.BASE_URL + slugify(fh.full_name) + "-" + str(fh.id)
            trun_subject = fh.subject[:100] + '...'

            # Convert UTC datetime from db to user's local datetime.
            submitted_date = convert_to_local_datetime(fh.submitted)

            edit_url = settings.BASE_URL + "basic_crud/" + str(fh.id) + "/change/"

            fh_list.append(
                    {'full_name': (fh.full_name),
                     'subject': trun_subject,
                     'email': fh.email,
                     'submitted': submitted_date,
                     'id': fh.id,
                     'url': url,
                     'edit_url': edit_url
                     })

        fh_data = fh_list
        json_data = json.dumps(fh_data)
        return JsonResponse(json_data, safe=False)

First, we need to standardize the date format from a Python way of formatting ‘%Y-%m-%d‘ and that means an equivalent to the yyyy-mm-dd format.

Moreover, we’ve got the user’s selected date range at this line.

1
2
3
# Get the date range values from the user input
mStartDate = request.POST.get('mStartDate')
mEndDate = request.POST.get('mEndDate')

However, the user’s date range inputs are considered unaware timezone and most especially the time difference between the user’s current timezone compare to the UTC which is Django’s default timezone setting to internationalize the database storage for DateTime columns.

Now, at this line of code, the pytz Python library will be automatically converted for us and deliver the correct localize timezone from wherever the user might be.

1
2
unaware_start_date = datetime.strptime(mStartDate, date_format)
aware_start_date = pytz.utc.localize(unaware_start_date)

The 2nd line which is the end date must be converted as well but with the additional +1 day especially when dealing with date range period so that the end date will always cover the complete last date of that date range period.

1
2
3
unaware_end_date = datetime.strptime(mEndDate, date_format
                                     ) + timedelta(days=1)
aware_end_date = pytz.utc.localize(unaware_end_date)

Thus, when the Django QuerySet will be executed, we use this aware date and time variable to pass on to the query.

1
2
3
4
5
6
# Display data, using __range from Django's built-in functionality
data_lists = ContactUs.objects.filter(
    is_deleted=False,
    submitted__range=(aware_start_date,
                         aware_end_date)
    ).order_by('-id')[:50]

Besides, in this example, we use the submitted date and time column to query the table rows, and the built-in Django __range functionality to query between two dates.

Step 6: Create a new URL for a Date Range AJAX search event

Moreover, add a new URL from the dev/dev/urls.py when triggering the AJAX search date range event.

1
2
path('basic_search_dr/', myroot.views.vmain.basic_search_dr_view,
         name='basic_search_dr'),

Step 7: Modify the basic_crud_list.html to add the JQuery AJAX search event for Date Range criteria

Likewise, modify the dev/myroot/templates/myroot/basic_crud_list.html and add the following AJAX and JQuery event inside the $(document).ready(function().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//Search data by date range
$('.SearchDataDR').on("click",function()
{
    //GET THE VALUE
    var csrfmiddlewaretoken = Cookies.get('csrftoken');

    if (csrfmiddlewaretoken === undefined || csrfmiddlewaretoken === null || csrfmiddlewaretoken === ""){
        swal("CSRF Token is Missing!", "Please refresh this page and try again.", "error");
        return false;
    }

    $.ajax
    ({
        type: "POST",
        url: BASE_URL+"basic_search_dr/",

        <!--INSERT PARAMETERS HERE-->
        data: 'csrfmiddlewaretoken='+csrfmiddlewaretoken + '&mStartDate='+mStartDate + '&mEndDate='+mEndDate,
        cache: false,
        dataType: "json",
        success: function(jResults)
        {
            $('#tbData').empty();
            var table = $('#tblData').DataTable();
            table.clear();

            var rowStatus = '';
            var data = jQuery.parseJSON(jResults);
            $.each(data, function(i, obj) {

                var add_row = `<a href="{% url 'basic_crud_create' %}"><i class="fa fa-plus"></i></a>`;
                var edit_row = `<a href="`+ obj.edit_url +`"><i class="fa fa-edit"></i></a>`;
                var del_row = `<a href="javascript:void(0);" onclick="confirmUserDeleteAction(this);" row-id="`+ obj.id +`"><i class="far fa-trash-alt"></i></a>`;
                var visit_link = `<a href="`+ obj.url +`" target="_blank"><i class="fa fa-globe"></i></a>`;

                table.row.add([
                    obj.full_name,
                    obj.subject,
                    obj.email,
                    obj.submitted,
                    add_row,
                    edit_row,
                    del_row,
                    visit_link,
                    obj.id
                ]).node().id = 'tr'+obj.id;
                table.draw( false );
            });
        }
    });
});

The only difference between SearchDataText and SearchDataDR is their parameters, the SearchDataText pass on the input box, likewise, the SearchDataDR have two parameters which are the start and end date.

Next, bind the SearchDataDR JQuery function to the DateRangePicker object when the user selected the date range criteria and triggers this SearchDataDR function to execute immediately.

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="col-6 d-none d-sm-block">
    <div class="form-group mb-0 pl-2 mt-3 float-right">
        <div class="input-group">
            <button type="button" class="btn btn-default" id="daterange-btn">
                <span><i class="fa fa-calendar"></i> Filter by date range</span>
                <i class="fa fa-caret-down"></i>
            </button>
            <div class="input-group-append">
                <button type="button" class="btn btn-secondary SearchDataDR"><i class="fa fa-search"></i></button>
            </div>
        </div>
    </div>
</div>

Finally, upload all the changes we have made to your own web server using FileZilla. Afterward, reload your daphne web service to see the changes we’ve done.

1
daphne -b 172.104.190.249 -p 8000 dev.asgi:application

Now, once again, open the Django Basic CRUD: Manage Table Rows using DataTables page and try the DateTimePicker object to select between dates and see how it behaves and surely when you hit that search button, it will NOT reload the entire page, instead, it automatically redraws the DataTables object each row and load the JSON results as well.

Native Django Search Form Submission for better SEO (Search Engine Optimization) Method

Since, the native form submission has existed for a long time when any programming languages were created and it’s been supported ever since before AJAX or any JavaScript was sprouted.

The great benefits of why most of the public pages out there have been using the native Django form submission when dealing with the search results is the Search Engine Optimization (SEO) which is very friendly for most of the search engines like Google, Bing, Yahoo, etc.

So, we’ll be continuing the step-by-step guides for this topic.

Step 8: Import the two built-in Django libraries

First, open once again the dev/myroot/views/vmain.py and import the following Django libraries for further use later on.

1
2
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.shortcuts import redirect

The Django Paginator will be used for limiting the number of page rows in each of page links and the Django Redirect will be used to redirect to a different page for some reason and things alike.

Step 9: Create a new View for the Django Search form Submission

Now, still at the dev/myroot/views/vmain.py and add this code snippet below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def search_view(request):
    """
    Renders the native Django search with form post submission
    with basic SEO compliance.
    """

    if request.method == "GET":
        fsearch = request.GET.get('q')

        if fsearch and len(fsearch) >= settings.MIN_CHARS_SEARCH:

            # Filter data by using __icontains built-in Django function
            data_lists = ContactUs.objects.filter(
                            Q(is_deleted=False) &
                            (Q(full_name__icontains=fsearch) |
                            Q(email__icontains=fsearch) |
                            Q(subject__icontains=fsearch) |
                            Q(message__icontains=fsearch))).order_by('-id')[:50]

            paginator = Paginator(data_lists, 50)  # Show 50 rows per page
            page = request.GET.get('page', 1)

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

            # Get the index of the current page
            index = data_pages.number - 1  # edited to something easier without index
            max_index = len(paginator.page_range)
            start_index = index - 5 if index >= 5 else 0
            end_index = index + 5 if index <= max_index - 5 else max_index
            page_range = list(paginator.page_range)[start_index:end_index]
            totRows = "{:,}".format(paginator.count)

            return render(request, 'myroot/search.html',
                          {
                              'title': 'Search Results for: ' + str(fsearch),
                              'meta_desc': 'These are the list of search results based on your search text criteria.',
                              'data_pages': data_pages,
                              'page_range': page_range,
                              'totRows': totRows,
                              'q': fsearch
                           })
        else:
            return redirect('emptysearch')

Here, at this line of code, this is an optional condition but it’s recommended that can set some minimum characters requirement before the user tries to search with empty text to prevent some bulk of data rows or reduce the stress of the database back-end processing of heavy memory usage from the web server.

1
if fsearch and len(fsearch) >= settings.MIN_CHARS_SEARCH:

Next, add the new config from the dev/dev/settings.py and insert at the bottom line.

1
2
3
4
SITE_CONTACT_US = "https://pinoylearnpython.com/get-in-touch-with-me/"

# Minimum characters for search
MIN_CHARS_SEARCH = 3

Moreover, add this two new config from the dev/myroot/global_config.py and the new source code would be exactly at the code snippet below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from django.conf import settings


def global_settings(request):
    """ Return custom constant global variables to be
    used widely for all of our apps. """

    return{
        'BASE_URL': settings.BASE_URL,
        'SITE_SHORT_NAME': settings.SITE_SHORT_NAME,
        'SITE_FULL_NAME': settings.SITE_FULL_NAME,
        'SITE_YEAR_STARTED': settings.SITE_YEAR_STARTED,
        'SITE_URL_HOME': settings.SITE_URL_HOME,
        'SITE_SLOGAN': settings.SITE_SLOGAN,
        'SITE_CONTACT_US': settings.SITE_CONTACT_US,
        'MIN_CHARS_SEARCH': settings.MIN_CHARS_SEARCH,
    }

At this line of code, this will be a required to give the number of rows per page link to give chunks of data to make it faster to load even though we’ve many rows fetch from the Django QuerySet results.

1
2
paginator = Paginator(data_lists, 50)  # Show 50 rows per page
page = request.GET.get('page', 1)

You can probably change the 50 rows per page and it’s up to you and of course the default first page will be given as always at the 2nd line.

However, this is some small configuration for Django Pagination link buttons and how it behaves during multiple pages retrieve.

1
2
3
4
5
6
7
# Get the index of the current page
index = data_pages.number - 1  # edited to something easier without index
max_index = len(paginator.page_range)
start_index = index - 5 if index >= 5 else 0
end_index = index + 5 if index <= max_index - 5 else max_index
page_range = list(paginator.page_range)[start_index:end_index]
totRows = "{:,}".format(paginator.count)

In short, this value of 5, is the number of pagination buttons will be created during the user’s navigation, but you can always change this with your own liking.

Step 10: Create a new URL for Django Search Form Submission

Now, open the dev/dev/urls.py and add a new URL for Django Search form submission.

1
2
path('search/', myroot.views.vmain.search_view,
     name='search'),

As usual, we need to tie this up with an HTML page to cater a landing page.

Step 11: Create a new Django Template for the Native Search Form Submission

Again, create a new HTML file and save it as search.html from dev/myroot/templates/myroot/ directory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
{% extends "myroot/layout_home.html" %}
{% load static %}
{% load humanize %}

{% block extra_styles_head %}{% endblock %}

{% block page_sub_title %}
<div class="jumbotron">
<div class="container">
<h1 class="display-3">Native Django Search Form Submission for better SEO (Search Engine Optimization) Method</h1>
This is the native Django form submission using post method to query records from the database with Django pagination.

<a class="btn btn-primary btn-lg" href="https://pinoylearnpython.com/basic-django-search-queries-with-pagination/" role="button">Learn more »</a>
<a class="btn btn-secondary btn-lg" href="https://dev.pinoylearnpython.com/django-basic-crud-list/" role="button">Back to Django Manage Table Rows »</a>

<section>
<div class="d-flex flex-column flex-md-row align-items-center p-1 px-md-4 mb-0">
<div class="container">
<div class="row justify-content-center text-center section-intro">
<div class="col-12 col-md-9 col-lg-8 mb-0">

                            <form id="formSearch" action="/search/" method="get" class="card p-2">
<div class="input-group">
                                    <input id="q" name="q" value="{{ q }}" type="text" class="form-control" placeholder="Search records from dev_contact_us table">
<div class="input-group-append">
                                        <button type="submit" class="btn btn-primary" id="btnSearch">Search</button></div>
</div>
</form>

</div>
</div>
</div>
</div>
</section>

</div>
</div>
{% endblock %}

{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12 order-md-1">
<table class="table table-striped table-valign-middle">
<tbody>
                        {% if data_pages %}
{% for s in data_pages %}
<tr>
<td>
                                        <a href="{% url 'basic_crud_dyn_pub_page' s.full_name|slugify s.id %}" target="_blank">{{ s.full_name }}</a>
{{ s.subject }}</td>
<td>
                                        <span class="d-flex flex-column text-right">
{{ s.submitted|naturaltime }}
</span></td>
</tr>
{% endfor %}
{% endif %}</tbody>
</table>
{% if data_pages %}
<nav aria-label="Page navigation mt-3" id="navPagination">
<ul class="pagination justify-content-end">
                            {% if data_pages.has_previous %}
  <li class="page-item"><a class="page-link" href="?q={{ q }}&page=1">First</a></li>
  <li class="page-item"><a class="page-link" href="?q={{ q }}&page={{ data_pages.previous_page_number }}">Previous</a></li>
{% endif %}

{% for pg in page_range %}
{% if data_pages.number == pg %}
  <li class="page-item active"><span class="page-link">{{ pg }}<span class="sr-only">(current)</span></span></li>
{% else %}
  <li class="page-item"><a class="page-link" href="?q={{ q }}&page={{ pg }}">{{ pg }}</a></li>
{% endif %}
{% endfor %}

{% if data_pages.has_next %}
  <li class="page-item"><a class="page-link" href="?q={{ q }}&page={{ data_pages.next_page_number }}">Next</a></li>
  <li class="page-item"><a class="page-link" href="?q={{ q }}&page={{ data_pages.paginator.num_pages }}">Last</a></li>
{% endif %}</ul>
</nav>                {% endif %}

</div>
</div>
</div>
{% endblock %}

{% block scripts %}{% endblock %}

The good thing about this search form submission is you can easily re-use the code below to any of your HTML pages to attach this search tool.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<section>
    <div class="d-flex flex-column flex-md-row align-items-center p-1 px-md-4 mb-0">
        <div class="container">
            <div class="row justify-content-center text-center section-intro">
                <div class="col-12 col-md-9 col-lg-8 mb-0">

                    <form id="formSearch" action="/search/" method="get" class="card p-2">
                        <div class="input-group">
                            <input id="q" name="q" value="{{ q }}" type="text" class="form-control" placeholder="Search records from dev_contact_us table">
                            <div class="input-group-append">
                                <button type="submit" class="btn btn-primary" id="btnSearch">Search</button>
                            </div>
                        </div>
                    </form>

                </div>
            </div>
        </div>
    </div>
</section>

Thus, I recommend you to create a separate HTML file and paste the code above and include this HTML file to anywhere you want the search tool to appear.

Step 12: Create a new View for Empty / Invalid Search

Meanwhile, the Django form submission must have a separate page to redirect to when some search criteria like the empty input box and less than the minimum of characters specified from our settings.py custom configuration did not meet the required conditions. Again, open the dev/myroot/views/vmain.py to add a new view.

1
2
3
4
5
6
7
8
def emptysearch_view(request):
    """Renders the empty search page."""
    if request.method == 'GET':
        return render(request, 'myroot/empty_search.html',
                      {'title': "Oops! Invalid Search.",
                       'meta_desc': """Either you forget to enter your search text criteria
                       or at least key-in the minimum of 3 characters for the search operation
                       to proceed. Thank You!"""
})

Since, earlier at step no. 9, we have this if statement to satisfy the condition before the Django QuerySet must be executed.

1
2
3
4
if fsearch and len(fsearch) >= settings.MIN_CHARS_SEARCH:
    ...execute the Django QuerySet here
else:
    return redirect('emptysearch')

Otherwise, it will redirect to another page to let the user know that the search is unsuccessful.

Step 13: Create a new URL for the Empty / Invalid Search

Next, open the dev/dev/urls.py and add a new URL for the redirecting page for invalid search criteria.

1
2
path('emptysearch/', myroot.views.vmain.emptysearch_view,
     name='emptysearch'),

As usual, we need to tie this up again with an HTML page to cater a landing page.

Step 14: Create a new Django Template for the Empty / Invalid redirecting page

Again, create a new HTML file and save it as empty_search.html from the dev/myroot/templates/myroot/directory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 {% extends "myroot/layout_home.html" %} {% load static %} {% block schema_json %}{% endblock %} {% block extra_styles_head %}{% endblock %} {% block page_sub_title %}
<div class="page-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<h1 class="display-4">Oops! Invalid Search.</h1>
<p class="lead">Either you forget to enter your search text criteria or at least key-in the minimum of {{ MIN_CHARS_SEARCH }} characters for the search operation to proceed. Thank You!</p>

<section>
<div class="d-flex flex-column flex-md-row align-items-center p-1 px-md-4 mb-0">
<div class="container">
<div class="row justify-content-center text-center section-intro">
<div class="col-12 col-md-9 col-lg-8 mb-0"><form id="formSearch" class="card p-2" action="/search/" method="get">
<div class="input-group"><input id="q" class="form-control" name="q" type="text" value="{{ q }}" placeholder="Search at least {{ MIN_CHARS_SEARCH }} minimum characters." />
<div class="input-group-append"><button id="btnSearch" class="btn btn-primary" type="submit">Search</button></div>
</div>
</form></div>
</div>
</div>
</div>
</section><a class="btn btn-primary my-2" href="https://pinoylearnpython.com/basic-django-search-queries-with-pagination/">Learn more »</a> <a class="btn btn-secondary my-2" href="{{ SITE_CONTACT_US }}">Contact Us</a>

</div>
{% endblock %} {% block content %}{% endblock %} {% block scripts %}{% endblock %}

Finally, upload all the new and modified files to your web server using a FileZilla and make it sure that you have reloaded your daphne web service to implement the changes.

1
daphne -b 172.104.190.249 -p 8000 dev.asgi:application

Once again, check out the Django Basic CRUD: Manage Table Rows using DataTables page to try using the native Django search form and experience the way it behaves as well.

You can download the source code from our GitHub repositories at https://github.com/pinoylearnpython/dev and stay tuned for any updates.

In the next event loop on {{ PLP }}.

Congratulations!, what a hard day for us, but it’s all worth it to completely finish the Django Basic Search and how exactly it works perfectly fine with the actual examples that I have provided just for you.

Indeed, I have covered two methods and what’s their main usage and purpose to apply for your Django powered site and implement it accordingly to your future or existing Django projects.

For those who’re not able to successfully launch your Django Basic Search tutorial or you need more clarifications, don’t worry, leave a comment below and I’m happy to help you to succeed.

See you in the next event loop on The Automatic Admin Interface – Django Admin Site.

That’s all, have fun learning with {{ PLP }}.

To help Filipino students to learn Python programming language with Django to enhance their capabilities in developing robust web-based applications with practical and direct to the point tutorials, step-by-step with actual information that I provided for you. Leave a comment below or email me at [email protected], thank you!