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

A search box that executes on each key press. 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

directory, which is installed as per the above “detailed instructions” link: and Django project directory, which you should create now:

/home/myname/django_files/django_ajax_demo

Install the Django project

Start your virtualenv :

source /home/myname/django_files/django_ajax_demo_venv/bin/activate (exit it with deactivate ) Create the project directory:

mkdir /home/myname/django_files/django_ajax_demo/ 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/ Create the sub-application: cd /home/myname/django_files/django_ajax_demo/ 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 In

/home/myname/django_files/django_ajax_demo/django_ajax_demo/settings.py Add 'django.contrib.humanize' and 'color_liker' to INSTALLED_APPS ( 'humanize' is used in the template) 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:

python manage.py makemigrations 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 "<code>{{ search_text }}</code>":</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!

Start the webserver: For Nginx/Gunicorn (may be executed in or out of the virtualenv ): sudo service nginx start 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: Make sure you’re in the virtualenv :

source /home/myname/django_files/django_ajax_demo_venv/bin/activate cd /home/myname/django_files/django_ajax_demo/ 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)