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

In the previous post, we created a simple Django “colors” website (here is a JavaScript-only demonstration), which allows you to search the colors by substring, and to declare one or more to be your favorite. Currently, every action taken causes the entire page to be reloaded.

[All parts: ONE, TWO, THREE]

Now let’s change it so only the portion being changed–the search-results or the pressed like button–is refreshed. This will be achieved by implementing Ajax with JQuery. In addition:

  • Instead of having to press the "search" button, a new search will be executed automatically at every key-press.
  • We’ll implement some modest protection against rapid-fire requests by
    • Ignoring key-presses on the search form if they come less than 100 milliseconds after the previous.
    • Ignoring clicks on the same like button, if it occurs less than one-half second after the most-recently-processed click.

In this post, we’ll do some necessary preparations and update the like buttons. In the next post, we’ll update the search feature.

(A reminder to backup your work before proceeding.)

Create a public static-file directory

Since these changes require a substantial amount of JavaScript, it will be placed in a publicly-available and static location. Because of this, we might as well also move the stylesheet out of the web page and into the same directory. Finally, although we could import JQuery directly from the internet, I’m choosing download it locally (version 1.11.1).

Below is my setup. You’ll have to tailor these instructions as necessary for your system.

In
    /home/myname/django_files/django_ajax_demo/django_ajax_demo/settings.py
create-or-replace the following variables, and then, if you have an Nginx server, follow the instructions in the comments:

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/

STATIC_URL = '/color_liker/static/'
STATIC_ROOT = ""
"""
Put the following in
    `/etc/nginx/sites-available/django_ajax_demo`
----------------------------------------
server {
    server_name my.websites.ip.or.domain;

    access_log on;

    #Django JQuery Demo...START

    #Static images, js, css
    location /color_liker/static/ {
       alias /home/myname/django_files/django_ajax_demo/static/;
    }

    #Static images for the admin
    location /color_liker/static/admin/ {
       alias /home/myname/django_files/django_ajax_demo_venv/lib/python3.4/site-packages/django/contrib/admin/static/admin/;
    }

    #Django JQuery Demo...END

   location / {
      proxy_pass http://127.0.0.1:8001;
      proxy_set_header X-Forwarded-Host $server_name;
      proxy_set_header X-Real-IP $remote_addr;
      add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
   }
}
----------------------------------------

This prevents nginx passing the request to the WSGI/Django app
server. Static files need no processing, so nginx handles them
directly.
"""
STATICFILES_DIRS = (
    #Put strings here, like "/home/html/static" or "C:/www/django/static".
    #Always use forward slashes, even on Windows.
    #Don't forget to use absolute paths, not relative paths.
    ("assets", BASE_DIR + "/static"),
    os.path.join('static'),
)

Create the directory
    /home/myname/django_files/django_ajax_demo/static/
and restart your server.

To confirm your setup, create a file named
    /home/myname/django_files/django_ajax_demo/static/temp.txt
containing something. Then in your browser, you should be able to see that text at the url
    http://my.website/color_liker/static/temp.txt

Delete the file before proceeding.

Download JQuery 1.11.1 into the just-created static directory

Download link (right-click and "Save as…"): http://code.jquery.com/jquery-1.11.1.min.js

Changing the views

File:
    /home/myname/django_files/django_ajax_demo/color_liker/views.py

In the original toggle_color_like function, once a like is processed (saved to the database), the request is then redirected to the main ColorList view:

return  redirect("color_list")  #See urls.py

This results in the entire page being reloaded, thus reflecting the new like-state. In order to refresh only the like button, that part of the template must be moved into its own file (as described below). For the view to directly render it, the above redirect line should be changed to

    #Render the just-clicked-on like-button.
    return  render_to_response("color_liker/color_like_link__html_snippet.txt",
                               {"color": color})

As a consequence of this change, the import statement at the top of the same file, which used to be

from django.shortcuts     import redirect

must be changed to

from django.shortcuts     import render_to_response

Update the template and stylesheet

[The original template]

In order to individually refresh the just-clicked-on like button, it needs to be moved out of the main template and into its own file.

<button class="button_{% if not color.is_favorited %}un{% endif %}liked"
   >{% if color.is_favorited %}Yes{% else %}No{% endif %}</button>

This sub-template is used by both the main page load, as it creates the color table, and by the Ajax call, for refreshing a single color. Save it as
    /home/myname/django_files/django_ajax_demo/color_liker/templates/color_liker/color_like_link__html_snippet.txt

The main template

There are three changes in the template:

  1. The like button is now "include"-d from a sub-template,
  2. The static directory is "load"-ed and used to import both JQuery and our yet-to-be-created JavaScript
  3. The stylesheet has also been moved to the static directory (and somewhat updated).

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">
      <form id="search_colors_form_id" method="get" action="{% url 'color_list' %}">
         <input type="text" id="search_text" name="search_text"/>
         {# csrf_token is not needed when the method is "get" #}
         <input id="id_pic_submit_button" type="submit" value="Search for color"/>
         <p>(Requires {{ MIN_SEARCH_CHARS|apnumber }} or more characters)</p>
      </form>

      {% 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 %}
   </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
         variable (the url to toggle the like) needs to be set from the
         Django variable. While this could be hard-coded into the
         JavaScript, this allows it to be centrally located (in
         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_like.js' %}"></script>
</body></html>

The stylesheet

Save the following as
    /home/myname/django_files/django_ajax_demo/static/stylesheet.css

.search_section  {
   float: left;
}
.content_section  {
   float: left;
}
table  {
   border:1px solid #000;
   border-spacing: 0px;
   background-color: #EEE;
   border-collapse:collapse;
}
thead  {
   font-weight: bold;
   text-decoration: underline;
}
th  {
   border:1px solid #000;
   padding: 4px;
}
/*
   All table cells except for the yes/no buttons, should have 4px of
   padding.

   The yes/no-buttons should completely fill the cell, so the cell
   itself (appears to) behave like a button.
 */
td  {
   text-align: center;
   vertical-align: middle;
   padding: 4px;
   border: 1px solid #000;
}
.td__toggle_color_like_button  {
   padding: 0px;
}
button  {
   border: 0px;
   /*
      So the buttons completely fill their containers--the table cell.
    */
   width: 100%;
   height: 100%;
}
.td__color_color  {
   text-align: right;
}
.td__color_name  {
   text-align: left;
}
.td__color_is_liked  {
   text-align: center;
}
.button_liked  {
   background-color: #CD0;
}
.button_unliked  {
   background-color: white;
}

The Ajax/JQuery JavaScript

The below code implements Ajax by attaching "click" events to each like button, and intercepting those clicks to decide if they should be forwarded onto Django. If the click is too soon (since the last actually-processed click) it is ignored. It then receives the response from Django, and uses it to update (a specific portion of) the web page.

The benefit of using JQuery to do these things, is that the same code works for all browsers.

Without Ajax:

With Ajax:

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

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

/*
  This file must be imported immediately-before the close-</body> tag,
  and after JQuery and Underscore.js are imported.
*/
/**
  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;
/**
   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);
};
/**
   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_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()  {
  /*
    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));
  /*
    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".
   */
});

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. Notice that clicking on the like buttons no longer reloads the page, and clicking as fast as you can on any one (like) button will not hit the server more than twice a second.

[All parts: ONE, TWO, THREE]

(If you need help, leave a comment!)

Update the search feature to Ajax

Now let’s implement Ajax into the search feature.

…to be continued…

(cue cliffhanger segue music)

Advertisements

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

  1. ben

    /home/myname/django_files/django_ajax_demo/color_liker/color_liker/templates/color_liker/color_like_link__html_snippet.txt

    is that really the correct directory for this? up till this point in the tutorial that direct has not been created

    Reply
    1. aliteralmind Post author

      No. Should be

      /home/myname/django_files/django_ajax_demo/color_liker/templates/color_liker/color_like_link__html_snippet.txt

      About to fix it. Thank you reporting it.

      Reply
  2. ben

    i’m getting a color table with the favorited yes/no button visible. however clicking on the button does not change the apperance. the page must be reloaded before the apperance of each button accurately reflects the Favorited status of each color

    Reply
  3. ben

    found it.

    theres a typo between the variable color_like_link__html_snippet.txt and color__like_link__html_snippet.txt

    need to be consistent with number of underscores between color and like

    Reply
    1. aliteralmind Post author

      As far as I can tell, they’re all correctly “color_like_link__html_snippet.txt”. I can’t find “color__like_link__html_snippet.txt” anywhere in the tutorial.

      Reply
  4. zeusbeso

    The color id you supply to generate a django url doesn’t have to be bogus does it?
    Would it just have to fit whatever regex you want to capture in urls.py?

    Reply
  5. quanghuy147

    Hi, I read your code , I tried it. It is successful. Thank you very much. However, I have one thing that I don’t understand. Can you explain for me?

    That is you commented in the code: $(this) is the button, but when I use console.log($(this)) . It shows that “this” is not the button but the . I wonder is there any link between the clicked button and the div? So javascripts automatically understands that when we click on the button in the , the scripts for that div would be run. How can that happen?

    In addition, how can the javascript function “processLikeknow when to run? I see no listener has been set like : button.onclick(function)….

    Thank you for your tutorial !

    Huy.

    Reply
    1. aliteralmind Post author

      I’m unsure of your question regarding the commented code, and don’t have the time to figure that out at the moment. Hopefully someone else can chime in about that one.

      However the processLike function is activated at the bottom of the JavaScript file, under the document-ready section (the last file on this webpage, above). Specifically, it’s activated on click of one of the like buttons.

      You’re welcome for the tutorial. I’m glad it helped you.

      Reply
      1. quanghuy147

        Thank you aliteralmind! I really appreciate your help and your time. Sorry to confuse you with my question. I will try to search for more information about Handling events with divs in Javascript and HTML. Thank you again! Have a nice day!

  6. Taylor F

    Hi there! Thanks for this great tutorial! I implemented this in my django project for two different use cases, one was simple and successful (Yay!), but now the second is giving an error because I’m trying to pass in an extra variable.

    I have added in the .js file an extra variable like so:
    var item_id = $button_just_clicked_on.data(‘item_id’);
    var object_id = $button_just_clicked_on.data(‘object_id’);

    then in views.py i tried to pass the extra variable in:

    def redeem_points(request, item_id, object_id):
    print(“This ran”)

    but that raises a 500 error and the function doesn’t run at all.

    Any info on how to pass more variables to my function without breaking it would be appreciated!

    Thanks again!

    Reply
    1. Taylor F

      I just figured it out! Yay!

      So the solution was to pass the new object_id variable shown above into the config variable as data like so:

      var config = {
      url: ITEM_URL_PRE_ID + item_id + ‘/’,
      dataType: ‘html’,
      data: {‘item_id’: item_id, ‘object_id’: object_id},
      success: processServerResponse,
      ….

      which attaches it to the url as GET data ( e.g …/item/?item_id=2&object_id=1 )

      then in the views.py function use the GET call:

      object_id = request.GET.get(‘object_id’)

      And that does the trick!

      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