Note
A newer version of this tutorial using Django 1.9 is available from Leanpub: https://leanpub.com/tangowithdjango19
AJAX essentially is a combination of technologies that are integrated together to reduce the number of page loads. Instead of reloading the full page, only part of the page or the data in the page is reloaded. If you haven’t used AJAX before or would like to know more about it before using it, check out the resources at the Mozilla website: https://developer.mozilla.org/en-US/docs/AJAX
To simplify the AJAX requests, we will be using the JQuery library. Note that if you are using the Twitter CSS Bootstrap toolkit then JQuery will already be added in. Otherwise, download the latest version of JQuery and include it within your application (see Chapter ..).
To make the interaction with the Rango application more seamless let’s add in a number of features that use AJAX, such as:
Create a new file, called rango-ajax.js and add it to your js directory. Then in your base template include:
<script src="{% static "js/jquery.js" %}"></script>
<script src="{% static "js/rango-ajax.js" %}"></script>
Here we assume you have downloaded a version of the JQuery library, but you can also just directly refer to it:
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
Now that the pre-reqs for using JQuery are in place we can use it to pimp the rango application.
It would be nice to let user, who are registered, denote that they “like” a particular category. In the following workflow, we will let users “like” categories, but we will not be keeping track of what categories they have “liked”, we’ll be trusting them not to click the like button multiple times.
To let users “like” certain categories undertake the following workflow:
To prepare the template we will need to add in the “Like” button with id="like" and create a <div> to display the number of likes {{% category.likes %}}. To do this, add the following <div> to the category.html template:
<p>
<strong id="like_count">{{ category.likes }}</strong> people like this category
{% if user.is_authenticated %}
<button id="likes" data-catid="{{category.id}}" class="btn btn-primary" type="button">
<span class="glyphicon glyphicon-thumbs-up"></span>
Like
</button>
{% endif %}
</p>
Create a view called, like_category in rango/views.py which will examine the request and pick out the category_id and then increment the number of likes for that category.
from django.contrib.auth.decorators import login_required
@login_required
def like_category(request):
cat_id = None
if request.method == 'GET':
cat_id = request.GET['category_id']
likes = 0
if cat_id:
cat = Category.objects.get(id=int(cat_id))
if cat:
likes = cat.likes + 1
cat.likes = likes
cat.save()
return HttpResponse(likes)
On examining the code, you will see that we are only allowing authenticated users to denote that they like a category. The view assumes that a variable category_id has been passed through via a GET so that the we can identify the category to update. In this view, we could also track and record that a particular user has “liked” this category if we wanted - but we are keeping it simple to focus on the AJAX mechanics.
Don’t forget to add in the URL mapping, into rango/urls.py. Update the urlpatterns by adding in:
url(r'^like_category/$', views.like_category, name='like_category'),
Now in “rango-ajax.js” you will need to add some JQuery code to perform an AJAX GET request. Add in the following code:
$('#likes').click(function(){
var catid;
catid = $(this).attr("data-catid");
$.get('/rango/like_category/', {category_id: catid}, function(data){
$('#like_count').html(data);
$('#likes').hide();
});
});
This piece of JQuery/Javascript will add an event handler to the element with id #likes, i.e. the button. When clicked, it will extract the category id from the button element, and then make an AJAX GET request which will make a call to /rango/like_category/ encoding the category_id in the request. If the request is successful, then the HTML element with id like_count (i.e. the <strong> ) is updated with the data returned by the request, and the HTML element with id likes (i.e. the <button>) is hidden.
There is a lot going on here and getting the mechanics right when constructing pages with AJAX can be a bit tricky. Essentially here, when the button is clicked an AJAX request is made, given our url mapping, this invokes the like_category view which updates the category and returns the new number of likes. When the AJAX request receives the response it updates parts of the page, i.e. the text and the button. The #likes button is hidden.
It would be really neat if we could provide a fast way for users to find a category, rather than browsing through a long list. To do this we can create a suggestion component which lets users type in a letter or part of a word, and then the system responds by providing a list of suggested categories, that the user can then select from. As the user types a series of requests will be made to the server to fetch the suggested categories relevant to what the user has entered.
To do this you will need to do the following.
Instead of creating a template called suggestions.html re-use the cats.html as it will be displaying data of the same type (i.e. categories).
To let the client ask for this data, you will need to create a URL mapping; lets call it category_suggest
With the mapping, view, and template for this view in place, you will need to update the base.html template and add in some javascript so that the categories can be displayed as the user types.
Above this <div> add an input box for a user to enter the letters of a category, i.e.:
<input class="input-medium search-query" type="text" name="suggestion" value="" id="suggestion" />
In this helper function we use a filter to find all the categories that start with the string supplied. The filter we use will be istartwith, this will make sure that it doesn’t matter whether we use upper-case or lower-case letters. If it on the other hand was important to take into account whether letters was upper-case or not you would use startswith instead.
def get_category_list(max_results=0, starts_with=''):
cat_list = []
if starts_with:
cat_list = Category.objects.filter(name__istartswith=starts_with)
if cat_list and max_results > 0:
if cat_list.count() > max_results:
cat_list = cat_list[:max_results]
return cat_list
Using the get_category_list function we can now create a view that returns the top 8 matching results as follows:
def suggest_category(request):
cat_list = []
starts_with = ''
if request.method == 'GET':
starts_with = request.GET['suggestion']
cat_list = get_category_list(8, starts_with)
return render(request, 'rango/cats.html', {'cat_list': cat_list })
Note here we are re-using the rango/cats.html template :-).
Add the following code to urlpatterns in rango/urls.py:
url(r'^suggest_category/$', views.suggest_category, name='suggest_category'),
In the base template in the sidebar div add in the following HTML code:
<ul class="nav nav-list">
<li class="nav-header">Find a Category</li>
<form>
<label></label>
<li><input class="search-query span10" type="text" name="suggestion" value="" id="suggestion" /></li>
</form>
</ul>
<div id="cats">
</div>
Here we have added in an input box with id="suggestion" and div with id="cats" in which we will display the response. We don’t need to add a button as we will be adding an event handler on keyup to the input box which will send the suggestion request.
Add the following JQuery code to the js/rango-ajax.js:
$('#suggestion').keyup(function(){
var query;
query = $(this).val();
$.get('/rango/suggest_category/', {suggestion: query}, function(data){
$('#cats').html(data);
});
});
Here, we attached an event handler to the HTML input element with id="suggestion" to trigger when a keyup event occurs. When it does the contents of the input box is obtained and placed into the query variable. Then a AJAX GET request is made calling /rango/category_suggest/ with the query as the parameter. On success, the HTML element with id=”cats” i.e. the div, is updated with the category list html.
To let registered users quickly and easily add a Page to the Category put an “Add” button next to each search result.
Create a view auto_add_page that accepts a parameterised GET request (title, url, catid) and adds it to the category
Map an url to the view url(r'^auto_add_page/$', views.auto_add_page, name='auto_add_page'),
Add an event handler to the button using JQuery - when added hide the button. The response could also update the pages listed on the category page, too.
HTML Template code:
{% if user.is_authenticated %}
<button data-catid="{{category.id}}" data-title="{{ result.title }}" data-url="{{ result.link }}" class="rango-add btn btn-mini btn-info" type="button">Add</button>
{% endif %}
JQuery code:
$('.rango-add').click(function(){
var catid = $(this).attr("data-catid");
var url = $(this).attr("data-url");
var title = $(this).attr("data-title");
var me = $(this)
$.get('/rango/auto_add_page/', {category_id: catid, url: url, title: title}, function(data){
$('#pages').html(data);
me.hide();
});
});
Note here we are assigned the event handler to all the buttons with class rango-add.
View code:
@login_required
def auto_add_page(request):
cat_id = None
url = None
title = None
context_dict = {}
if request.method == 'GET':
cat_id = request.GET['category_id']
url = request.GET['url']
title = request.GET['title']
if cat_id:
category = Category.objects.get(id=int(cat_id))
p = Page.objects.get_or_create(category=category, title=title, url=url)
pages = Page.objects.filter(category=category).order_by('-views')
# Adds our results list to the template context under name pages.
context_dict['pages'] = pages
return render(request, 'rango/page_list.html', context_dict)