The step-by-step tutorial I wish I had when learning Ajax in Django (with JQuery) — Part three of three

In part one of this tutorial, we made a simple “colors” website in which you can search based on substrings, and choose one or more as your favorites (here is a JavaScript-only demonstration). In part two, we updated the like buttons with Ajax, using JQuery. Ajax provides the benefit of reloading only the just-pressed button, instead of the whole page.

[All parts: ONE, TWO, THREE]

In this final part, we’re also going to implement Ajax into the search feature. In addition, we’ll make the search execute automatically on each key-press, and prevent rapid-fire requests, by ignoring key-presses less than one-tenth-of-a-second apart.

(A reminder to backup your work before proceeding.)

Changing the views

In the original code, the search is implemented as a logic branch in the main ColorList view. If a GET is detected

        if(self.request.method == "GET"):

(and there’s at least two characters) then a search is executed. Otherwise, it’s just a "normal" request, without interactivity–in other words: just load the page.

In order to refresh only the search results, that portion of the main template must be separated out into it’s own file, and its corresponding view code must be moved out of the ColorList and into its own (function-based) view.

Do this by replacing the existing single ColorList (class-based) view in
    /home/myname/django_files/django_ajax_demo/color_liker/views.py
with the following two views:

class ColorList(ListView):
    """
    Displays all colors in a table with only two columns: the name
    of the color, and a "like/unlike" button.
    """
    model = Color
    context_object_name = "colors"

    def dispatch(self, request, *args, **kwargs):
        return super(ColorList, self).dispatch(request, *args, **kwargs)

    def get_queryset(self):
        """
        This returns the all colors, for display in the main table.

        The search result query set, if any, is passed as context.
        """
        return  super(ColorList, self).get_queryset()

    def get_context_data(self, **kwargs):
        #Get the current context.
        context = super(ColorList, self).get_context_data(**kwargs)

        context["MIN_SEARCH_CHARS"] = MIN_SEARCH_CHARS

        return  context

def submit_color_search_from_ajax(request):
    """
    Processes a search request, ignoring any where less than two
    characters are provided. The search text is both trimmed and
    lower-cased.

    See <link to MIN_SEARCH_CHARS>
    """

    colors = []  #Assume no results.

    global  MIN_SEARCH_CHARS

    search_text = ""   #Assume no search
    if(request.method == "GET"):
        search_text = request.GET.get("color_search_text", "").strip().lower()
        if(len(search_text) < MIN_SEARCH_CHARS):
            """
            Ignore the search. This is also validated by
            JavaScript, and should never reach here, but remains
            as prevention.
            """
            search_text = ""

    #Assume no results.
    #Use an empty list instead of None. In the template, use
    #   {% if color_search_results.count > 0 %}
    color_results = []

    if(search_text != ""):
        color_results = Color.objects.filter(name__contains=search_text)

    #print('search_text="' + search_text + '", results=' + str(color_results))

    context = {
        "search_text": search_text,
        "color_search_results": color_results,
        "MIN_SEARCH_CHARS": MIN_SEARCH_CHARS,
    };

    return  render_to_response("color_liker/color_search_results__html_snippet.txt",
                               context)

Update the templates

Create the search-results sub-template

[The template from parts one and two]

In order to render the search results separately, they must be moved from the main template into its own sub-template.

Save the following as
    /home/myname/django_files/django_ajax_demo/color_liker/color_liker/templates/color_liker/color_search_results__html_snippet.txt

{% if  search_text|length >= MIN_SEARCH_CHARS %}
   <p><b>Searching for &quot;<code>{{ search_text }}</code>&quot;:</b>
   {% if  color_search_results.count > 0 %}
      </p>
      <ul>
         {% for  color in color_search_results %} {# No colon after "color_search_results" #}
            <li>{{ color.name }}</li>
         {% endfor %}
      </ul>
   {% else %}
      <i>No results</i></p>
   {% endif %}
{% endif %}

Change the main template

The changes:

  1. The search-results are no longer in the main template,
  2. Instead, they are now represented in the main template by a div, which is populated (and un-populated) by the yet-to-be-created JavaScript
  3. The search form is no longer a form–it’s only a text box,
  4. The JavaScript imports are different, and require two additional Django variables to be assigned to JavaScript variables, before the imports.

Replace the contents of
    /home/myname/django_files/django_ajax_demo/color_liker/templates/color_liker/color_list.html
with

{% comment %}
   humanize:
      For the "apnumber" filter, to display "two" instead of
      "2". Requries 'django.contrib.humanize' in INSTALLED_APPS

   static:
      To access the public static file directory, without having to hard
      code urls.
{% endcomment %}
{% load humanize %}
{% load static %}
<!DOCTYPE html>
<html lang="en">
   <head>
      <title>Color Likenatorizer</title>
      <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
      <meta name="viewport" content="width=device-width"/>
      <link rel="stylesheet" type="text/css" href="{% static 'stylesheet.css' %}">
   </head>
<body>
   <div class="search_section">
      <!--
         The form is submitted on every key press ("keyup") to the ajax
         function. If the number of characters is greater than
         MIN_SEARCH_CHARS, then it is submitted to the Django view. Django
         then renders the sub-template

         color_search_results__html_snippet.txt

         whose output is fed back to the ajax function. Ajax then populates
         the rendered sub-template into the below div.

         This no longer needs to be a form since the JavaScript directly
         reads both fields (that is, it attaches event listeners to them,
         which automatically react to key-presses). To be clearer, I've
         added "color_" to the beginning of the text field's id.

         Notes:
            - csrf_token-s are not required in get requests.
            - Only because the view expects a GET request, the search may
            be directly tested with
               http://my.website/color_liker/search?color_search_text=bl
      -->
      <input type="text" id="color_search_text" name="search_text"/>
      <p>(Requires {{ MIN_SEARCH_CHARS|apnumber }} or more characters)</p>
      <div id="color_search_results"></div>
   </div>
   <div class="content_section">
      <h1>Color Likenatorizer</h1>

      {% if  colors.count == 0 %}
         <p><i>There are no colors in the database.</i></p>
      {% else %}
         <table>
            <tr>
               <th colspan="2">Color</th>
               <th>Favorite?</th>
            </tr>
               <!--
                  The yes/no like button is contained in the third column.
                  The stylesheet eliminates the button's border and expands
                  its width and height to 100%, so it fills its entire
                  container: the table cell. It therefore appears to *be*
                  the table cell.

                  The table cell is in this main template, the button in
                  it, is in the "include"d sub-template. The button
                  sub-template is used by the below for-loop, and also by
                  the toggle_color_like view, which is called by Ajax.

                  Ajax calls Django, which renders the sub-template and
                  feeds it back to Ajax, which then replaces the current
                  button/sub-template with the new one.

                  (The data-color_id is how the id is passed to JQuery. See
                  http://api.jquery.com/data/ )
               -->
            {% for  color in colors %} {# No colon after "colors" #}
               <tr>
                  <td style="background-color: {{ color.name }};" class="td__color_color">{{ color.name }}</td>
                  <td class="td__color_name">{{ color.name }}</td>
                  <td id="toggle_color_like_cell_{{ color.id }}" class="td__toggle_color_like_button" data-color_id="{{ color.id }}">
                     {% include "color_liker/color_like_link__html_snippet.txt" %}
                  </td>
               </tr>
            {% endfor %}
         </table>
      {% endif %}
   </div>
   <script type='text/javascript' src="{% static 'jquery-1.11.1.min.js' %}"></script>
   <script type='text/javascript' src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>
   <script language="javascript">
      /*
         Before our JavaScript can be imported, the following JavaScript
         variables need to be set from Django variables. While these
         values could be hard-coded into the JavaScript, this allows
         them to be centrally located.
      */
      //From color_liker.views.MIN_SEARCH_CHARS
      var MIN_SEARCH_CHARS = {{ MIN_SEARCH_CHARS }};

      //The url to submit the search form. From color_liker.urls
      var SUBMIT_URL = "{% url 'color_list' %}";

      /*
         the url to toggle the like. From color_liker.urls

         Since an id must be provided to the Django url, give it a
         bogus one, then immediately lop it off (along with the
         ending '/'). It is re-appended by the JavaScript.
      */
      var LIKE_URL_PRE_ID = "{% url 'toggle_color_like' color_id='999999999' %}"
      LIKE_URL_PRE_ID = LIKE_URL_PRE_ID.substring(0, LIKE_URL_PRE_ID.length - "999999999/".length);
   </script>
   <script type='text/javascript' src="{% static 'color_ajax_search.js' %}"></script>
   <script type='text/javascript' src="{% static 'color_ajax_like.js' %}"></script>
   <script type='text/javascript' src="{% static 'color_ajax_main.js' %}"></script>
</body></html>

Update the URL configuration

There is a new view for the search form, so a new url must point to it. Replace the contents of
    /home/myname/django_files/django_ajax_demo/color_liker/urls.py
with

from django.conf.urls  import patterns, include, url
from color_liker.views import ColorList

urlpatterns = patterns('',
    url(r"^$", ColorList.as_view(), name="color_list"),
    url(r"^like_color_(?P<color_id>\d+)/$", "color_liker.views.toggle_color_like", name="toggle_color_like"),
    url(r"^search/$", "color_liker.views.submit_color_search_from_ajax", name="color_list"),
)

Ajax: JavaScript

The JavaScript for the search feature will be placed in a new file. Before creating it, lets split the single existing JavaScript file, containing both the like button and "main" functionality, into two. While all this JavaScript code could easily be placed into a single file (without comments or empty lines, there’s only about 50 lines of code), I find it clearer to split them into three: main, like, and search.

This code now uses Underscore.js. The original code doesn’t.

File one: The like button

The "main" function is moved out of this file, and into the one below.

File name:
    /home/myname/django_files/django_ajax_demo/static/color_ajax_like.js

//THIS FILE MUST BE IMPORTED BEFORE THE "main" FILE.

/**
   Executes a like click. Triggered by clicks on the various yes/no links.
 */
var processLike = function()  {

   //In this scope, "this" is the button just clicked on.
   //The "this" in processServerResponse is *not* the button just clicked
   //on.
   var $button_just_clicked_on = $(this);

   //The value of the "data-color_id" attribute.
   var color_id = $button_just_clicked_on.data('color_id');

   var processServerResponse = function(sersverResponse_data, textStatus_ignored,
                            jqXHR_ignored)  {
      //alert("sf sersverResponse_data='" + sersverResponse_data + "', textStatus_ignored='" + textStatus_ignored + "', jqXHR_ignored='" + jqXHR_ignored + "', color_id='" + color_id + "'");
      $('#toggle_color_like_cell_' + color_id).html(sersverResponse_data);
   }

   var config = {
      url: LIKE_URL_PRE_ID + color_id + '/',
      dataType: 'html',
      success: processServerResponse
      //Should also have a "fail" call as well.
   };
   $.ajax(config);
};

File two: The "main" function

This includes the original main function, with the addition of one that attaches a key-press listener to the search text box. The new function is called by both the main and the below search-JavaScript file.

Save the following as:
    /home/myname/django_files/django_ajax_demo/static/color_ajax_main.js

//THIS MUST BE IMPORTED AS THE VERY LAST THING BEFORE THE CLOSE </body>
//tag.

/**
  The number of milliseconds to ignore key-presses in the search box,
  after a key *that was not ignored* was pressed. Used by
  `$(document).ready()`.

  Equal to <code>100</code>.
 */
var MILLS_TO_IGNORE_SEARCH = 100;
/**
  The number of milliseconds to ignore clicks on the *same* like
  button, after a button *that was not ignored* was clicked. Used by
  `$(document).ready()`.

  Equal to <code>500</code>.
 */
var MILLS_TO_IGNORE_LIKES = 500;
/**
   The Ajax "main" function. Attaches the listeners to the elements on
   page load, each of which only take effect every
   <link to MILLS_TO_IGNORE_SEARCH> or <link to MILLS_TO_IGNORE_LIKES>
   seconds.

   This protection is only against a single user pressing buttons as fast
   as they can. This is in no way a protection against a real DDOS attack,
   of which almost 100% bypass the client (browser) (they instead
   directly attack the server). Hence client-side protection is pointless.

   - http://stackoverflow.com/questions/28309850/how-much-prevention-of-rapid-fire-form-submissions-should-be-on-the-client-side

   The protection is implemented via Underscore.js' debounce function:
  - http://underscorejs.org/#debounce

   Using this only requires importing underscore-min.js. underscore-min.map
   is not needed.
 */
$(document).ready(function()  {
  /*
    Warning: Placing the true parameter outside of the debounce call:

    $('#color_search_text').keyup(_.debounce(processSearch,
        MILLS_TO_IGNORE_SEARCH), true);

    results in "TypeError: e.handler.apply is not a function"
   */
  $('#color_search_text').keyup(_.debounce(processSearch,
      MILLS_TO_IGNORE_SEARCH, true));
  /*
    There are many buttons having the class

      td__toggle_color_like_button

    This attaches a listener to *every one*. Calling this again
    would attach a *second* listener to every button, meaning each
    click would be processed twice.
   */
  $('.td__toggle_color_like_button').click(_.debounce(processLike,
      MILLS_TO_IGNORE_LIKES, true));
});

File three: The search feature

Save the following as:
    /home/myname/django_files/django_ajax_demo/static/color_ajax_search.js

///THIS FILE MUST BE IMPORTED BEFORE THE "main" FILE.
/**
  Executes a search for colors containing a substring.
 */
var processSearch = function()  {
  //The key-press listener is no longer attached

  //Get and trim the search text.
  var searchText = $('#color_search_text').val().trim().toLowerCase();

  if(searchText.length < MIN_SEARCH_CHARS)  {
    //Too short. Ignore the submission, and erase any current results.
    $('#color_search_results').html("");

  }  else  {
    //There are at least two characters. Execute the search.

    var processServerResponse = function(sersverResponse_data, textStatus_ignored,
                        jqXHR_ignored)  {
      //alert("sersverResponse_data='" + sersverResponse_data + "', textStatus_ignored='" + textStatus_ignored + "', jqXHR_ignored='" + jqXHR_ignored + "'");
      $('#color_search_results').html(sersverResponse_data);
    }

    var config = {
      /*
        Using GET allows you to directly call the search page in
        the browser:

        http://the.url/search/?color_search_text=bl

        Also, GET-s do not require the csrf_token
       */
      type: "GET",
      url: SUBMIT_URL,
      data: {
        'color_search_text' : searchText,
      },
      dataType: 'html',
      success: processServerResponse
    };
    $.ajax(config);
  }
};

That’s it! Give it a try!

Start your server, and go to
    http://my.website/color_liker

Once again, you should see something like this. The obvious difference is the page no longer reloads on each search request. To see the "rapid-fire request protection" in action, dramatically increase the value of MILLS_TO_IGNORE_SEARCH.

[All parts: ONE, TWO, THREE]


Thank you for going through this tutorial.

If you need help or have any suggestions, please leave a comment below. I’ll be glad to assist you.

Advertisements

33 thoughts on “The step-by-step tutorial I wish I had when learning Ajax in Django (with JQuery) — Part three of three

  1. trendsetter37

    In this third section of the tutorial I think you have the ajax search function in your color_ajax_like.js file as opposed to having the the processLike function there instead, resulting in two processSearch functions overall and nothing to handle the like button.

    Am I seeing that right or missing something.

    Reply
    1. aliteralmind Post author

      Last week I upgraded the JavaScript to use Underscore.js. I manually duplicated my working code to the posts, and didn’t notice I messed it up. It’s a pain, this manual copying. In the tutorial I’m working on now, on Django authentication, I have a Java build for each chapter. It automatically reads in the code snippets and inserts them into the post template.

      Thanks for telling me. Going to fix it right now.

      Reply
  2. ravi

    thank you for this, its very helpful to me …..please guide me how to upload files and email sending through ajax calls in django…. without using forms.py

    Reply
    1. aliteralmind Post author

      I am glad this helped you, ravi.

      Although I have done both uploading of files and email, I’ve done neither via AJAX (or without forms.py). I am sorry I will not be able to help you with this.

      Reply
      1. ravi

        thank you for responding, i think without forms it is not posible ,i want to uploading of files and email, give some ideas about AJAX ,now i want use forms and models also
        thank you
        ravi

  3. Chris Newell

    Thanks you for a great tutorial! I enjoyed this immensely. I will be trying to use the skills learned from this tutorial to build a “HUD” like semi real-time dashboard for my raspberry pi and sensors including automatic plotting into google maps from a GPS module.

    Reply
  4. Chantwaffle

    Great tutorial, thank you for sharing your knowledge in such a simple way:) Just one question, you put 2 urls with the same value for “name” argument, how does the django know that when calling “var SUBMIT_URL = “{% url ‘color_list’ %}”;” we want “color_liker/search/” not just “color_liker/” ?

    Reply
    1. aliteralmind Post author

      That’s a darn good question. Unfortunately, I don’t have the opportunity to test this this weekend. My instinct says that the search one should be “color_search” instead of “color_list”, and the SUBMIT_URL value should also be made to point to it.

      I can only guess, not being at a computer, that the second one in urls.py, the search one, overrides the first, which is why it still works. But that makes me wonder if the “main” link to the list itself (as far as the end user is concerned, there’s only one page in the site, right?), is not just linking to the page, but rather to a search, IN WHICH THERE HAPPENS TO BE NO SEARCH TERM. So effectively, it works in all cases, but really shouldn’t be doing what it’s doing.

      I hope I’m being clear, and I wonder if that’s all correct. Either way, great catch! If you happen to figure out what’s going on before I do, please let me know! I haven’t had the chance to do Django or Python in months :( I now have a Java-only job.

      Reply
  5. Uter Knuter

    This is awesome, really, thank you! Your tutorial works, however, if I want to include it into my project, the results are not displayed (no error message or anything) and if I look into the terminal the “GET /search/?cars_search_text=…” is all in pink and the status says 500 for server error. What did I do wrong? I changed all names, the website is up and running, search results however don’t work.
    Is it because of my urls? it looks like this:
    #stuff that is imported

    import cars.views as carsviews

    urlpatterns=[
    url(r’^$’, carsviews.LandingView.as_view(), name=’index’),
    url(r’^search/$’, ‘cars.views.submit_company_search_from_ajax’, name=’index’),
    ]

    Reply
  6. Uter Knuter

    But I have a new question, do I have to put the search part in a separate .txt? I want that something happens when you click on the search results which I am handling with Javascript. And now that the search part is handled in a different file, my JS code doesn’t listen to it anymore (Before in part one with the submit button it worked).

    Reply
    1. aliteralmind Post author

      Not entirely sure of the question. The search results template needs to be in a separate text file, as described under “Create the search-results sub-template”.

      This is required by the Ajax response.

      Reply
      1. Uter Knuter

        Hi, yeah I got it, I want to so something with the search result in the page but I wasn’t sure how to get it since it’s not in the DOM tree when the page is loaded. BUT i found out how, with jQuery:

        $(‘body’).on(‘click’,’idofthesearchresults’,function(){
        myFunction
        });

  7. Uter Knuter

    Ok, and what would I do to get only one result, for example the first one? I tried this in the views

    color_results = Color.objects.filter(name__contains=search_text).order_by(‘id’).first()

    and

    color_results = Color.objects.filter(name__contains=search_text)[0]

    but didn’t work

    Reply
  8. Uter Knuter

    Randomly, when I use it I get an internal Server error and it’s not showing results, how could I debug this?

    Reply
  9. Uter Knuter

    Actually, I do have a question. I use your code to look up locations in a database. SOme of the locations have reviews but not all of them. So when I give out the location and reviews. Inthe .txt file it looks like this:
    {% for location in location_search_results %}
    {{location.name}}
    {{location.review}}
    {% endfor %}

    So if there is no review, the console throws an error, is there any way to avoid this? Let’s say with a if {{% %}}.exists()

    I tried finding that online but couldn’t.

    Reply
  10. Jamie

    Thanks for the tutorial! As someone still coming to grips with django and ajax etc this has been immensely helpful, looking forward to going through more of your work!

    Reply
  11. Adam

    This was an extremely helpful, comprehensive and well written tutorial – the way you anticipate problems is truly amazing! You deserve some kind of award.

    One typo, the javascript variable serverResponse_data is misspelled.

    Reply
  12. Pingback: The step-by-step tutorial I wish I had when learning Ajax in Django (with JQuery) — Part three of three – anhuysite

  13. evelynfl

    This tutorial is what I’ve been looking for days, very well written and rich in features/examples. Two small things I’d like to mention:

    1. The urlpatterns doesn’t work with Django 1.10. Using
    from color_liker.views import toggle_color_like

    url(r”^like_color_(?P\d+)/$”, toggle_color_like, name=”toggle_color_like”),
    instead works fine

    2. According to this https://docs.djangoproject.com/en/1.10/intro/tutorial01/ tutorial it’s good practice to place the static files into a sub-directory with the same name as the main app dir so it doesn’t get mixed up in the production environment with static files from other apps with identical names. This would be django_ajax_demo/color_liker/static/color_liker/ in this example. The paths in the templates need to be updated to static color_liker/ for this to work

    Thanks again for this great and very helpful tutorial, you’re a BIG help.

    Reply
  14. asherrill

    Hi, it’s really a nice tut. If you are still optimizing this tut I would like to point out a few things:

    1) “from django.conf.urls import patterns” I have the feeling that “patterns” doesn’t exist anymore in django. I’m using Django version 1.11

    2) On this site “Update the URL configuration” has wrong code I guess. Since your overwrite the code in views.py the function “toggle_color_like” doesn’t exist anymore. Should replace it with the function “submit_color_search_from_ajax”

    Best regards,

    A

    Reply
    1. asherrill

      Sorry, looks like I misunderstood your instruction regarding the changes in the view and so also regarding the necessary changes in the url.py – only the thing wir the “patterns” is still true.

      Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s