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

This tutorial demonstrates implementing Ajax in Django, using JQuery, for two specific tasks:

  1. A search box that executes on each key press.
  2. A like button that says "yes" if it’s liked, and "no" if it’s not.

[All parts: ONE, TWO, THREE]

Updated 2/5/2015:

Take a look at a mockup of the website we’re going to be creating, which is written in Knockout. (Here it is again as a JSFiddle.)

In addition, we’ll add in some basic protection against rapid-fire requests, by preventing the search form from submitting until at least two characters are provided, and by ignoring clicks of the same like-button if it is pressed again within a half-second.

First we’ll create the website without Ajax, and then we’ll integrate the Ajax calls.

Learning Django and JQuery by themselves

Before going through this tutorial, you’ll need a basic understanding of both Django (and Python!) and JQuery (and JavaScript!).

This is how I learned Django. My Django support is primarily on irc #django and by asking questions on Stack Overflow.

I learned Python by reading the official tutorial and Dive Into Python 3, running through the Coding Bat and codecademy tutorials, and especially by immersing myself in Django.

I learned JQuery by reading much of the documentation on the JQuery Learning Center, and then by running through the codecademy tutorial. I’m familiar with JavaScript, but have not professionally worked with it, so I read through the HTML Dog JavaScript tutorials (only read them) and also ran through the codecademy tutorial.

Depending on your background, all of these things are highly recommended.

Now…

My setup

  • Operating system: Ubuntu 14.04.1 x32
  • Web server: Nginx 1.4.6
  • Database: Postgres 9.3.5
  • WSGI app server: Gunicorn 19.1.0
  • Django 1.7
  • Python 3.4.0
  • JQuery 1.11.1

I have Django installed into a virtualenv that uses Python 3.4 only. Here are detailed instructions for installing Nginx, Postgres, Gunicorn, and Django onto a Digital Ocean web server.

My directory structure is based on these two folders:

  • The virtualenv directory, which is installed as per the above “detailed instructions” link:
        /home/myname/django_files/django_ajax_demo_venv
  • and Django project directory, which you should create now:
        /home/myname/django_files/django_ajax_demo

Install the Django project

  1. Start your virtualenv:
        source /home/myname/django_files/django_ajax_demo_venv/bin/activate     (exit it with deactivate)
  2. Create the project directory:
        mkdir /home/myname/django_files/django_ajax_demo/
  3. Create your project (this is a long command that belongs on a single line):
        django-admin.py startproject django_ajax_demo /home/myname/django_files/django_ajax_demo/

  4. Create the sub-application:
    1. cd /home/myname/django_files/django_ajax_demo/
    2. python manage.py startapp color_liker

     
    This and the previous command create the following (items unused by this tutorial are omitted):

    $ tree /home/myname/django_files/django_ajax_demo/
    +-- color_liker
    ¦   +-- admin.py
    ¦   +-- models.py
    ¦   +-- views.py
    +-- django_ajax_demo
    ¦   +-- settings.py
    ¦   +-- urls.py
    +-- manage.py
  5. In
        /home/myname/django_files/django_ajax_demo/django_ajax_demo/settings.py

    1. Add 'django.contrib.humanize' and 'color_liker' to INSTALLED_APPS ('humanize' is used in the template)
    2. Configure your database by overwriting the current value with
      DATABASES = {
          'default': {
              'ENGINE': 'django.db.backends.postgresql_psycopg2',
              'NAME': 'database_name_here',
              'USER': 'database_username_here',
              'PASSWORD': 'database_user_password_goes_here',
              'HOST': "localhost",  # Empty for localhost through domain sockets or
                                    # '127.0.0.1' for localhost through TCP.
              'PORT': '',           # Set to empty string for default.
          }
      }

Create your model

Replace the contents of
    /home/myname/django_files/django_ajax_demo/color_liker/models.py
with

from django.db import models

class Color(models.Model):
    """
    The color's name (as used by the CSS 'color' attribute, meaning
    lowercase values are required), and a boolean of whether it's "liked"
    or not. There are NO USERS in this demo webapp, which is why there's no
    link/ManyToManyField to the User table.

    This implies that the website is only useable for ONE USER. If multiple
    users used it at the same time, they'd be all changing the same values
    (and would see each others' changes when they reload the page).
    """
    name = models.CharField(max_length=20)
    is_favorited = models.BooleanField(default=False)

    def __str__(self):
        return  self.name

    class Meta:
        ordering = ['name']

Register it with the admin app by replacing the contents of
    /home/myname/django_files/django_ajax_demo/color_liker/admin.py
with

from django.contrib import admin
from .models import Color

admin.site.register(Color)

and then sync it to the database:

  1. python manage.py makemigrations
  2. python manage.py migrate

Insert data into the database

If data already exists, delete it via the admin app (http://my.website/admin).

$ source /home/myname/django_files/django_ajax_demo_venv/bin/activate
$ cd /home/myname/django_files/django_ajax_demo/
$ python manage.py shell
>>> from color_liker.models import Color
>>> Color.objects.all()
[]
>>> colors = ["aqua", "black", "blue", "fuchsia", "gray", "green", "lime", "maroon", "navy", "olive", "orange", "purple", "red", "silver", "teal", "white", "yellow"]
>>> Color.objects.bulk_create(Color(name=name) for name in colors)
<generator object <genexpr> at 0xb5f8bacc>
>>> Color.objects.all()
[<Color: aqua>, <Color: black>, <Color: blue>, <Color: fuchsia>, <Color: gray>, <Color: green>, <Color: lime>, <Color: maroon>, <Color: navy>, <Color: olive>, <Color: orange>, <Color: purple>, <Color: red>, <Color: silver>, <Color: teal>, <Color: white>, <Color: yellow>]

The views

There are two views. The main class-based view that handles both the normal page load and search form submission, and a function-based view that handles the toggling of a color’s like-state.

Replace the contents of
    /home/myname/django_files/django_ajax_demo/color_liker/views.py
with

from django.shortcuts     import redirect
from django.views.generic import ListView
from color_liker.models   import Color

MIN_SEARCH_CHARS = 2
"""
The minimum number of characters required in a search. If there are less,
the form submission is ignored. This value is used by the below view and
the template.
"""
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):
        self.request = request     #So get_context_data can access it.
        return super(ColorList, self).dispatch(request, *args, **kwargs)

    def get_queryset(self):
        """
        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):
        #The current context.
        context = super(ColorList, self).get_context_data(**kwargs)

        global  MIN_SEARCH_CHARS

        search_text = ""   #Assume no search
        if(self.request.method == "GET"):
            """
            The search form has been submitted. Get the search text from
            it. If it's less than MIN_SEARCH_CHARS characters, ignore the
            request.

            Must be GET, not post.
            - http://stackoverflow.com/questions/25878993/django-view-works-with-default-call-but-form-submission-to-same-view-only-calls

            Also, must use

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

            not

                if(self.request.GET)

            https://docs.djangoproject.com/en/1.7/ref/request-response/#django.http.HttpRequest.method
            https://docs.djangoproject.com/en/1.7/ref/request-response/#django.http.HttpRequest.POST
            """
            search_text = self.request.GET.get("search_text", "").strip().lower()
            if(len(search_text) < MIN_SEARCH_CHARS):
                search_text = ""   #Ignore search

        if(search_text != ""):
            color_search_results = Color.objects.filter(name__contains=search_text)
        else:
            #An empty list instead of None. In the template, use
            #  {% if color_search_results.count > 0 %}
            color_search_results = []

        #Add items to the context:

        #The search text for display and result set
        context["search_text"] = search_text
        context["color_search_results"] = color_search_results

        #For display under the search form
        context["MIN_SEARCH_CHARS"] = MIN_SEARCH_CHARS

        return  context

def toggle_color_like(request, color_id):
    """Toggle "like" for a single color, then refresh the color-list page."""
    color = None
    try:
        #There's only one object with this id, but this returns a list
        #of length one. Get the first (index 0)
        color = Color.objects.filter(id=color_id)[0]
    except Color.DoesNotExist as e:
        raise  ValueError("Unknown color.id=" + str(color_id) + ". Original error: " + str(e))

    #print("pre-toggle:  color_id=" + str(color_id) + ", color.is_favorited=" + str(color.is_favorited) + "")

    color.is_favorited = not color.is_favorited
    color.save()  #Commit the change to the database

    #print("post-toggle: color_id=" + str(color_id) + ", color.is_favorited=" + str(color.is_favorited) + "")

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

The template

Create the directory
    Q:\django_files\django_ajax_demo\color_liker\templates\color_liker\
(yes, with the redundant "color_liker") and in it create a file named color_list.html, with these contents:

{% comment %}
   humanize: For the "apnumber" filter, to display "two" instead of
   "2". Requries 'django.contrib.humanize' in INSTALLED_APPS
{% endcomment %}
{% load humanize %}
<!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"/>
      <style type="text/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;
         }
         td  {
            text-align: center;
            vertical-align: middle;
            padding: 4px;
            border:1px solid #000;
         }
         .td__color_color  {
            text-align: right;
         }
         .td__color_name  {
            text-align: left;
         }
         .liked  {
            text-align: center;
            background-color: #CD0;
         }
         .unliked  {
            text-align: center;
            background-color: white;
         }
      </style>
   </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>
            {% 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 class="{% if not color.is_favorited %}un{% endif %}liked"
                     ><a href="{% url 'toggle_color_like' color.id %}"
                     >{% if color.is_favorited %}Yes{% else %}No{% endif %}</a></td>
               </tr>
            {% endfor %}
         </table>
      {% endif %}
   </div>
   <script language="javascript">
      document.getElementById("search_text").focus();
   </script>
</body></html>

Finally, configure the urls

Save the following text as
    /home/myname/django_files/django_ajax_demo/color_liker/urls.py

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

urlpatterns = patterns('',
    #Used as both the main page url, and for the search-form submission.
    #If the GET object exists, then the search-form is being submitted.
    #Otherwise, it's a normal page request.
    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"),
)

and replace the contents of
    /home/myname/django_files/django_ajax_demo/django_ajax_demo/urls.py
with

from django.conf.urls import patterns, include, url
from django.contrib   import admin

urlpatterns = patterns('',
    (r'^color_liker/', include('color_liker.urls')),
    url(r'^admin/', include(admin.site.urls)),
)

Give it a try!

  1. Start the webserver:
    • For Nginx/Gunicorn (may be executed in or out of the virtualenv):
      1. sudo service nginx start
      2. sudo /home/myname/django_files/django_ajax_demo_venv/bin/gunicorn -c /home/myname/django_files/django_ajax_demo_venv/gunicorn_config.py django_ajax_demo.wsgi:application     (the contents of the config file)
    • For the development-only Django server:
      1. Make sure you’re in the virtualenv:
            source /home/myname/django_files/django_ajax_demo_venv/bin/activate
      2. cd /home/myname/django_files/django_ajax_demo/
      3. python manage.py runserver
    • Open the browser and load the page:
          http://my.website/color_liker

You should see something like this. Everything you do on this page causes a full-page reload.

(Let me know in the comments if you have any problems, and I’ll be happy to help.)

Update the website for Ajax

Now let’s change it so that you don’t need to click the submit button, and the yes/no buttons reload only themselves–not the entire page.

[All parts: ONE, TWO, THREE]

At this point, it would be a good idea to backup your files.

…to be continued…

(cue cliffhanger segue music)

Advertisements

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

  1. Pingback: Дайджест новин мови Python #1 | Віталій Подоба

    1. Javis Sullivan

      Ahhh, Nvrmind it had something to do with my namespaced urls. If anyone else has something similar happen to them just know that your action=”{% url ‘namespace:name’ %}” form attribute in color_list.html needs to be correct or you will get a 400 bad request error.

      Great tutorial btw!

      Reply
    2. aliteralmind Post author

      I’d like to help you. The more information you can give me (where you are in the tutorial, specific error messages, anything else relevant you can think of), the better chance there is.

      Reply
  2. Jamie

    Hi there! Awesome tutorial, I’m struggling at one part though. I’m using sqlite3 and having trouble with the database importation / structure. Do you have a copy of the raw database file or even know how the adding data part would be best completed?

    Reply
  3. Pingback: Django全栈工程师学习资源 | 神刀安全网

  4. Pingback: 6.02 Django – 全栈Python

  5. dieuful

    Viewing your stackoverflow questions, it seems you had many of the same issues I had at one point. It’s funny though, the most constructive/viewed on there have almost no votes, meanwhile the self answered specifically the database migration roll back question had 7 votes.

    Is stackoverflow truly this broken? I know they have some issues with user participation and over participation but damn if that doesn’t make sense.

    Good blog post by the way.

    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