Enhancing the User Interface with Ajax

0
130
32 min read

Since our project is a Web 2.0 application, it should be heavily focused on the user experience. The success of our application depends on getting users to post and share content on it. Therefore, the user interface of our application is one of our major concerns. This article will improve the interface of our application by introducing Ajax features, making it more user-friendly and interactive.

Ajax and Its Advantages

Ajax, which stands for Asynchronous JavaScript and XML, consists of the following technologies:

  • HTML and CSS for structuring and styling information.
  • JavaScript for accessing and manipulating information dynamically.
  • XMLHttpRequest, which is an object provided by modern browsers for exchanging data with the server without reloading the current web page.
  • A format for transferring data between the client and server. XML is sometimes used, but it could be HTML, plain text, or a JavaScript-based format called JSON.

Ajax technologies let code on the client-side exchange data with the server behind the scenes, without having to reload the entire page each time the user makes a request. By using Ajax, web developers are able to increase the interactivity and usability of web pages.

Ajax offers the following advantages when implemented in the right places:

  • Better user experience. With Ajax, the user can do a lot without refreshing the page, which brings web applications closer to regular desktop applications.
  • Better performance. By exchanging only the required data with the server, Ajax saves bandwidth and increases the application’s speed.

There are numerous examples of web applications that use Ajax. Google Maps and Gmail are perhaps two of the most prominent examples. In fact, these two applications played an important role in spreading the adoption of Ajax, because of the success that they enjoyed. What sets Gmail from other web mail services is its user interface, which enables users to manage their emails interactively without waiting for a page reload after every action. This creates a better user experience and makes Gmail feel like a responsive and feature-rich application rather than a simple web site.

This article explains how to use Ajax with Django so as to make our application more responsive and user friendly. We are going to implement three of the most common Ajax features found in web applications today. But before that, we will learn about the benefits of using an Ajax framework as opposed to working with raw JavaScript functions.

Using an Ajax Framework in Django

In this section we will choose and install an Ajax framework in our application. This step isn’t entirely necessary when using Ajax in Django, but it can greatly simplify working with Ajax. There are many advantages to using an Ajax framework:

  • JavaScript implementations vary from browser to browser. Some browsers provide more complete and feature-rich implementations, whereas others contain implementations that are incomplete or don’t adhere to standards. Without an Ajax framework, the developer must keep track of browser support for the JavaScript features that they are using, and work around the limitations that are present in some browser implementations of JavaScript. On the other hand, when using an Ajax framework, the framework takes care of this for us; it abstracts access to the JavaScript implementation and deals with the differences and quirks of JavaScript across browsers. This way, we concentrate on developing features instead of worrying about browser differences and limitations.
  • The standard set of JavaScript functions and classes is a bit lacking for fully fledged web application development. Various common tasks require many lines of code even though they could have been wrapped in simple functions. Therefore, even if you decide not to use an Ajax framework, you will find yourself having to write a library of functions that encapsulates JavaScript facilities and makes them more usable. But why reinvent the wheel when there are many excellent Open Source libraries already available?

Ajax frameworks available on the market today range from comprehensive solutions that provide server-side and client-side components to light-weight client-side libraries that simplify working with JavaScript. Given that we are already using Django on the server-side, we only want a client-side framework. In addition, the framework should be easy to integrate with Django without requiring additional dependencies. And finally, it is preferable to pick a light and fast framework. There are many excellent frameworks that fulfil our requirements, such as Prototype, the Yahoo! UI Library and jQuery. I have worked with them all and they are all great. But for our application, I’m going to pick jQuery, because it’s the lightest of the three. It also enjoys a very active development community and a wide range of plugins. If you already have experience with another framework, you can continue using it during this article. It is true that you will have to adapt the JavaScript code in this article to your framework, but Django code on the server-side will remain the same no matter which framework you choose.

Now that you know the benefits of using an Ajax framework, we will move to installing jQuery into our project.

Downloading and Installing jQuery

One of the advantages of jQuery is that it consists of a single light-weight file. To download it, head to http://jquery.com/ and choose the latest version (1.2.3 at the time of writing). You will find two choices:

  • Uncompressed version: This is the standard version that I recommend you to use during development. You will get a .js file with the library’s code in it.
  • Compressed version: You will also get a .js file if you download this version. However, the code will look obfuscated. jQuery developers produce this version by applying many operations on the uncompressed .js file to reduce its size, such as removing white spaces and renaming variables, as well as many other techniques. This version is useful when you deploy your application, because it offers exactly the same features as the uncompressed one, but with a smaller file size.

I recommend the uncompressed version during development because you may want to look into jQuery’s code and see how a particular method works. However, the two versions offer exactly the same set of features, and switching from one to another is just a matter of replacing one file.

Once you have the jquery-xxx.js file (where xxx is the version number), rename it to jquery.js and copy it to the site_media directory of our project (Remember that this directory holds static files which are not Python code). Next, you will have to include this file in the base template of our site. This will make jQuery available to all of our project pages. To do so, open templates/base.html and add the highlighted code to the head section in it:

<head>
<title>Django Bookmarks |
{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="/site_media/style.css"
type="text/css" />
<script type="text/javascript"
src="/site_media/jquery.js"></script>
</head>

To add your own JavaScript code to an HTML page, you can either put the code in a separate .js file and link it to the HTML page by using the script tag as above, or write the code directly in the body of a script tag:

<script type="text/javascript">
// JavaScript code goes here.
</script>

The first method, however, is recommended over the second one, because it helps keep the source tree organized by putting HTML and JavaScript code in different files. Since we are going to write our own .js files during this article, we need a way to link .js files to templates without having to edit base.html every time. We will do this by creating a template block in the head section of the base.html template. When a particular page wants to include its own JavaScript code, this block may be overridden to add the relevant script tag to the page. We will call this block external, because it is used to link external files to pages. Open templates/base.html and modify its head section as follows:

<head>
<title>Django Bookmarks | {% block title %}{% endblock %}</title>
<link rel="stylesheet" href="/site_media/style.css"
type="text/css"/>
<script type="text/javascript" src="/site_media/jquery.js">
</script>
{% block external %}{% endblock %}
</head>

And we have finished. From now on, when a view wants to use some JavaScript code, it can link a JavaScript file to its template by overriding the external template block.

Before we start to implement Ajax enhancements in our project, let’s go through a quick introduction to the jQuery framework.

The jQuery JavaScript Framework

jQuery is a library of JavaScript functions that facilitates interacting with HTML documents and manipulating them. The library is designed to reduce the time and effort spent on writing code and achieving cross-browser compatibility, while at the same time taking full advantage of what JavaScript offers to build interactive and responsive web applications.

The general workflow of using jQuery consists of two steps:

  • Select an HTML element or a group of elements to work on.
  • Apply a jQuery method to the selected group

Element Selectors

jQuery provides a simple approach to select elements; it works by passing a CSS selector string to a function called $. Here are some examples to illustrate the usage of this function:

  • If you want to select all anchor (<a>) elements on a page, you can use the following function call: $(“a”)
  • If you want to select anchor elements which have the .title CSS class, use $(“a.title”)
  • To select an element whose ID is #nav, you can use $(“#nav”)
  • To select all list item (<li>) elements inside #nav, use $(“#nav li”)

And so on. The $() function constructs and returns a jQuery object. After that, you can call methods on this object to interact with the selected HTML elements.

jQuery Methods

jQuery offers a variety of methods to manipulate HTML documents. You can hide or show elements, attach event handlers to events, modify CSS properties, manipulate the page structure and, most importantly, perform Ajax requests.

Before we go through some of the most important methods, I highly recommend using the Firefox web browser and an extension called Firebug to experiment with jQuery. This extension provides a JavaScript console that is very similar to the interactive Python console. With it, you can enter JavaScript statements and see their output directly without having to create and edit files. To obtain Firebug, go to http://www.getfirebug.com/, and click on the install link. Depending on the security settings of Firefox, you may need to approve the website as a safe source of extensions.

If you do not want to use Firefox for any reason, Firebug’s website offers a “lite” version of the extension for other browsers in the form of a JavaScript file. Download the file to the site_media directory, and then include it in the templates/base.html template as we did with jquery.js:

<head>
<title>Django Bookmarks | {% block title %}{% endblock %}</title>
<link rel="stylesheet" href="/site_media/style.css"
type="text/css"/>
<script type="text/javascript" src="/site_media/firebug.js">
</script>
<script type="text/javascript" src="/site_media/jquery.js">
</script>
{% block external %}{% endblock %}
</head>

To experiment with the methods outlined in this section, launch the development server and navigate to the application’s main page. Open the Firebug console by pressing F12, and try selecting elements and manipulating them.

Hiding and Showing Elements

Let’s start with something simple. To hide an element on the page, call the hide() method on it. To show it again, call the show() method. For example, try this on the navigation menu of your application:

>>> $("#nav").hide()
>>> $("#nav").show()

Enhancing the User Interface with Ajax

You can also animate the element while hiding and showing it. Try the fadeOut(), fadeIn(), slideUp() or slideDown() methods to see two of these animated effects.

Of course, these methods (like all other jQuery methods) also work if you select more than one element at once. For example, if you open your user page and enter the following method call into the Firebug console, all of the tags will disappear:

>>> $('.tags').slideUp()

Accessing CSS Properties and HTML Attributes

Next, we will learn how to change CSS properties of elements. jQuery offers a method called css() for performing CSS operations. If you call this method with a CSS property name passed as a string, it returns the value of this property:

>>> $("#nav").css("display")
Result: "block"

If you pass a second argument to this method, it sets the specified CSS property of the selected element to the additional argument:

>>> $("#nav").css("font-size", "0.8em")
Result: <div id="nav" style="font-size: 0.8em;">

In fact, you can manipulate any HTML attribute and not just CSS properties. To do so, use the attr() method which works in a similar way to css(). Calling it with an attribute name returns the attribute value, whereas calling it with an attribute name/value pair sets the attribute to the passed value. To test this, go to the bookmark submission form and enter the following into the console:

>>> $("input").attr("size", "48")
Results:
<input id="id_url" type="text" size="48" name="url">
<input id="id_title" type="text" size="48" name="title">
<input id="id_tags" type="text" size="48" name="tags">

(Output may slightly differ depending on the versions of Firefox and Firebug). This will change the sizes of all input elements on the page at once to 48.

In addition, there are shortcut methods to get and set commonly used attributes, such as val() which returns the value of an input field when called without arguments, and sets this value to an argument if you pass one. There is also html() which controls the HTML code inside an element. Finally, there are two methods that can be used to attach or detach a CSS class to an element; they are called addClass() and removeClass(). A third method is provided to toggle a CSS class, and it is called toggleClass(). All of these class methods take the name of the class to be changed as a parameter.

Manipulating HTML Documents

Now that you are comfortable with manipulating HTML elements, let’s see how to add new elements or remove existing elements. To insert HTML code before an element, use the before() method, and to insert code after an element, use the after() method. Notice how jQuery methods are well-named and very easy to remember!

Let’s test these methods by inserting parentheses around tag lists on the user page. Open your user page and enter the following in the Firebug console:

>>> $(".tags").before("<strong>(</strong>")
>>> $(".tags").after("<strong>)</strong>")

You can pass any string you want to – before() or after() – the string may contain plain text, one HTML element or more. These methods offer a very flexible way to dynamically add HTML elements to an HTML document.

If you want to remove an element, use the remove() method. For example:

$("#nav").remove()

Not only does this method hide the element, it also removes it completely from the document tree. If you try to select the element again after using the remove() method, you will get an empty set:

>>> $("#nav")
Result: []

Of course, this only removes the elements from the current instance of the page. If you reload the page, the elements will appear again.

Traversing the Document Tree

Although CSS selectors offer a very powerful way to select elements, there are times when you want to traverse the document tree starting from a particular element. For this, jQuery provides several methods. The parent() method returns the parent of the currently selected element. The children() method returns all the immediate children of the selected element. Finally, the find() method returns all the descendants of the currently selected element. All of these methods take an optional CSS selector string to limit the result to elements that match the selector. For example, $(“#nav”).find (“li”) returns all the <li> descendants of #nav.

If you want to access an individual element of a group, use the get() method which takes the index of the element as a parameter. $(“li”).get(0) for example returns the first <li> element out of the selected group.

Handling Events

Next, we will learn about event handlers. An event handler is a JavaScript function that is invoked when a particular event happens, for example, when a button is clicked or a form is submitted. jQuery provides a large set of methods to attach handlers to events; events of particular interest in our application are mouse clicks and form submissions. To handle the event of clicking on an element, we select this element and call the click() method on it. This method takes an event handler function as a parameter. Let’s try this using the Firebug console. Open the main page of the application, and insert a button after the welcome message:

>>> $("p").after("<button id="test-button">Click me!</button>")

(Notice that we had to escape the quotations in the strings passed to the after() method.)

If you try to click this button, nothing will happen, so let’s attach an event handler to it:

>>> $("#test-button").click(function () { alert("You clicked me!"); })

Now, when you click the button, a message box will appear. How did this work? The argument that we passed to click() may look a bit complicated, so let’s examine it again:

function () { alert("You clicked me!"); }

This appears to be a function declaration but without a function name. Indeed, this construct creates what is called an anonymous function in JavaScript terminology, and it is used when you need to create a function on the fly and pass it as an argument to another function. We could have avoided using anonymous functions and declared the event handler as a regular function:

>>> function handler() { alert("You clicked me!"); }
>>> $("#test-button").click(handler)

The above code achieves the same effect, but the first one is more concise and compact. I highly recommend you to get used to anonymous functions in JavaScript (if you are not already), as I’m sure you will appreciate this construct and find it more readable after using it for a while.

Handling form submissions is very similar to handling mouse clicks. First, you select the form, and then you call the submit() method on it and pass the handler as an argument. We will use this method many times while adding Ajax features to our project in later sections.

Sending Ajax Requests

Before we finish this section, let’s talk about Ajax requests. jQuery provides many ways to send Ajax requests to the server. There is, for example, the load() method which takes a URL and loads the page at this URL into the selected element. There are also methods for sending GET or POST requests, and receiving the results. We will examine these methods in more depth while implementing Ajax features in our project.

What Next?

This wraps up our quick introduction to jQuery. The information provided in this section will be enough to continue with this article, and once you finish the article, you will be able to implement many interesting Ajax features on your own. But please keep in mind that this jQuery introduction is only the tip of the iceberg. If you want a comprehensive treatment of the jQuery framework, I highly recommend the book “Learning jQuery” from Packt Publishing, as it covers jQuery in much more detail. You can find out more about the book at:

http://www.packtpub.com/jQuery

Implementing Live Searching of Bookmarks

We will start introducing Ajax into our application by implementing live searching. The idea behind this feature is simple: when the user types a few keywords into a text field and clicks search, a script works behind the scenes to fetch search results and present them on the same page. The search page does not reload, thus saving bandwidth, and providing a better and more responsive user experience.

Before we start implementing this, we need to keep in mind an important rule while working with Ajax: write your application so that it works without Ajax, and then introduce Ajax to it. If you do so, you ensure that everyone will be able to use your application, including users who don’t have JavaScript enabled and those who use browsers without Ajax support.

Implementing Searching

So before we work with Ajax, let’s write a simple view that searches bookmarks by title. First of all, we need to create a search form, so open bookmarks/forms.py and add the following class to it:

class SearchForm(forms.Form):
query = forms.CharField(
label='Enter a keyword to search for',
widget=forms.TextInput(attrs={'size': 32})
)

As you can see, it’s a pretty straightforward form class with only one text field. This field will be used by the user to enter search keywords.

Next, let’s create a view for searching. Open bookmarks/views.py and enter the following code into it:

def search_page(request):
form = SearchForm()
bookmarks = [] show_results = False
if request.GET.has_key('query'):
show_results = True
query = request.GET['query'].strip()
if query:
form = SearchForm({'query' : query})
bookmarks =
Bookmark.objects.filter (title__icontains=query)[:10] variables = RequestContext(request, { 'form': form,
'bookmarks': bookmarks,
'show_results': show_results,
'show_tags': True,
'show_user': True
})
return render_to_response('search.html', variables)

Apart from a couple of method calls, the view should be very easy to understand. We first initialize three variables, form which holds the search form, bookmarks which holds the bookmarks that we will display in the search results, and show_results which is a Boolean flag. We use this flag to distinguish between two cases:

  • The search page was requested without a search query. In this case, we shouldn’t display any search results, not even a “No bookmarks found” message.
  • The search page was requested with a search query. In this case, we display the search results, or a “No bookmarks found” message if the query does not match any bookmarks.

We need the show_results flag because the bookmarks variable alone is not enough to distinguish between the above two cases. bookmarks will empty when the search page is requested without a query, and it will also be empty when the query does not match any bookmarks.

Next, we check whether a query was sent by calling the has_key method on the request.GET dictionary:

if request.GET.has_key('query'):
show_results = True
query = request.GET['query'].strip()
if query:
form = SearchForm({'query' : query})
bookmarks = Bookmark.objects.filter(title__icontains=query)[:10]

We use GET instead of POST here because the search form does not create or change data; it merely queries the database, and the general rule is to use GET with forms that query the database, and POST with forms that create, change or delete records from the database.

If a query was submitted by the user, we set show_results to True and call strip() on the query string to ensure that it contains non-whitespace characters before we proceed with searching. If this is indeed the case, we bind the form to the query and retrieve a list of bookmarks that contain the query in their title. Searching is done by using a method called filter in Bookmark.objects. This is the first time that we have used this method; you can think of it as the equivalent of a SELECT statements in Django models. It receives the search criteria in its arguments and returns search results. The name of each argument must adhere to the following naming convention:

field__operator

Note that field and operator are separated by two underscores: field is the name of the field that we want to search by and operator is the lookup method that we want to use. Here is a list of the commonly-used operators:

  • exact: The value of the argument is an exact match of the field.
  • contains: The field contains the value of the argument.
  • startswith: The field starts with the value of the argument.
  • lt: The field is less than the value of the argument.
  • gt: The field is greater than the value of the argument.

Also, there are case-insensitive versions of the first three operators: iexact, icontains and istartswith.

After this explanation of the filter method, let’s get back to our search view. We use the icontains operator to get a list of bookmarks that match the query and retrieve the first ten items using Python’s list slicing syntax. Finally we pass all the variables to a template called search.html to render the search page.

Now create the search.html template in the templates directory with the following content:

{% extends "base.html" %}
{% block title %}Search Bookmarks{% endblock %}
{% block head %}Search Bookmarks{% endblock %}
{% block content %}
<form id="search-form" method="get" action=".">
{{ form.as_p }}
<input type="submit" value="search" />
</form>
<div id="search-results">
{% if show_results %}
{% include 'bookmark_list.html' %}
{% endif %}
</div>
{% endblock %}

The template consists of familiar aspects that we have used before. We build the results list by including the bookmark_list.html like we did when building the user and tag pages. We gave the search form an ID, and rendered the search results in a div identified by another ID so that we can interact with them using JavaScript later. Notice how many times the include template tag saved us from writing additional code? It also lets us modify the look of the bookmarks list by editing a single file. This Django template feature is indeed very helpful in organizing and managing templates.

Before you test the new view, add an entry for it in urls.py:

urlpatterns = patterns('',
# Browsing
(r'^$', main_page),
(r'^user/(w+)/$', user_page),
(r'^tag/([^s]+)/$', tag_page),
(r'^tag/$', tag_cloud_page),
(r'^search/$', search_page),
)

Now test the search view by navigating to http://127.0.0.1:8000/search/ and experiment with it. You can also add a link to it in the navigation menu if you want; edit templates/base.html and add the highlighted code:

<div id="nav">
<a href="/">home</a> |
{% if user.is_authenticated %}
<a href="/save/">submit</a> |
<a href="/search/">search</a> |
<a href="/user/{{ user.username }}/">
{{ user.username }}</a> |
<a href="/logout/">logout</a>
{% else %}
<a href="/login/">login</a> |
<a href="/register/">register</a>
{% endif %}
</div>

We now have a functional (albeit very basic) search page. Thanks to our modular code, the task will turn out to be much simpler than it may seem.

Implementing Live Searching

To implement live searching, we need to do two things:

  • Intercept and handle the event of submitting the search form. This can be done using the submit() method of jQuery.
  • Use Ajax to load the search results in the back scenes, and insert them into the page. This can be done using the load() method of jQuery as we will see next.

jQuery offers a method called load() that retrieves a page from the server and inserts its contents into the selected element. In its simplest form, the function takes the URL of the remote page to be loaded as a parameter.

First of all, let’s modify our search view a little so that it only returns search results without the rest of the search page when it receives an additional GET variable called ajax. We do so to enable JavaScript code on the client-side to easily retrieve search results without the rest of the search page HTML. This can be done by simply using the bookmark_list.html template instead of search.html when request.GET contains the key ajax. Open bookmarks/views.py and modify search_page (towards the end) so that it becomes as follows:

def search_page(request):
[...] variables = RequestContext(request, {
'form': form,
'bookmarks': bookmarks,
'show_results': show_results,
'show_tags': True,
'show_user': True
})
if request.GET.has_key('ajax'):
return render_to_response('bookmark_list.html', variables)
else:
return render_to_response('search.html', variables)

Next, create a file called search.js in the site_media directory and link it to templates/search.html like this:

{% extends "base.html" %}
{% block external %}
<script type="text/javascript" src="/site_media/search.js">
</script>
{% endblock %}
{% block title %}Search Bookmarks{% endblock %}
{% block head %}Search Bookmarks{% endblock %}
[...]

Now for the fun part! Let’s create a function that loads search results and inserts them into the corresponding div. Write the following code into site_media/search.js:

function search_submit() {
var query = $("#id_query").val();
$("#search-results").load(
"/search/?ajax&query=" + encodeURIComponent(query)
);
return false;
}

Let’s go through this function line by line:

  • The function first gets the query string from the text field using the val() method.
  • We use the load() method to get search results from the search_page view, and insert the search results into the #search-results div. The request URL is constructed by first calling encodeURIComponent on query, which works exactly like the urlencode filter we used in Django templates. Calling this function is important to ensure that the constructed URL remains valid even if the user enters special characters into the text field such as &. After escaping query, we concatenate it with /search/?ajax&query=. This URL invokes the search_page view and passes the GET variables ajax and query to it. The view returns search results, and the load() method in turn loads the results into the #search-results div.
  • We return false from the function to tell the browser not to submit the form after calling our handler. If we don’t return false in the function, the browser will continue to submit the form as usual, and we don’t want that.

One little detail remains; where and when to attach search_submit to the submit event of the search form? A rule of a thumb when writing JavaScript is that we cannot manipulate elements in the document tree before the document finishes loading. Therefore, our function must be invoked as soon as the search page is loaded. Fortunately for us, jQuery provides a method to execute a function when the HTML document is loaded. Let’s utilize it by appending the following code to site_media/search.js:

$(document).ready(function () {
$("#search-form").submit(search_submit);
});

$(document) selects the document element of the current page. Notice that there are no quotations around document; it’s a variable provided by the browser, not a string. ready() is a method that takes a function and executes it as soon as the selected element finishes loading. So in effect, we are telling jQuery to execute the passed function as soon as the HTML document is loaded. We pass an anonymous function to the ready() method; this function simply binds search_submit to the submit event of the form #search-form.

That’s it. We’ve implemented live searching with less than fifteen lines of code. To test the new functionality, navigate to http://127.0.0.1:8000/search/, submit queries, and notice how the results are displayed without reloading the page:

Enhancing the User Interface with Ajax

The information covered in this section can be applied to any form that needs to be processed in the back scenes without reloading the page. You can, for example, create a comment form with a preview button that loads the preview in the same page without reloading. In the next section, we will enhance the user page to let users edit their bookmarks in place, without navigating away from the user page.

Editing Bookmarks in Place

Editing of posted content is a very common task in web sites. It’s usually implemented by offering an edit link next to content. When clicked, this link takes the user to a form located on another page where content can be edited. When the user submits the form, they are redirected back to the content page.

Imagine, on the other hand, that you could edit content without navigating away from the content page. When you click edit, the content is replaced with a form. When you submit the form, it disappears and the updated content appears in its place. Everything happens on the same page; edit form rendering and submission are done using JavaScript and Ajax. Wouldn’t such a workflow be more intuitive and responsive?

The technique described above is called in-place editing. It is now finding its way into web applications and becoming more common. We will implement this feature in our application by letting the user edit their bookmarks in place on the user page.

Since our application doesn’t support the editing of bookmarks yet, we will implement this first, and then modify the editing procedure to work in place.

Implementing Bookmark Editing

We already have most of the parts that are needed to implement bookmark editing. This was easy to do thanks to the get_or_create method provided by data models. This little detail greatly simplifies the implementation of bookmark editing. Here is what we need to do:

  • We pass the URL of the bookmark that we want to edit as a GET variable named url to the bookmark_save_page view.
  • We modify bookmark_save_page so that it populates the fields of the bookmark form if it receives the GET variable. The form is populated with the data of the bookmark that corresponds to the passed URL.

When the populated form is submitted, the bookmark will be updated as we explained earlier, because it will look like the user submitted the same URL another time.

Before we implement the technique described above, let’s reduce the size of bookmark_save_page by moving the part that saves a bookmark to a separate function. We will call this function _bookmark_save. The underscore at the beginning of the name tells Python not to import this function when the views module is imported. The function expects a request and a valid form object as parameters; it saves a bookmark out of the form data, and returns this bookmark. Open bookmarks/views.py and create the following function; you can cut and paste the code from bookmark_save_page if you like, as we are not making any changes to it except for the return statement at the end.

def _bookmark_save(request, form):
# Create or get link.
link, dummy =
Link.objects.get_or_create(url=form.clean_data['url'])
# Create or get bookmark.
bookmark, created = Bookmark.objects.get_or_create(
user=request.user,
link=link
)
# Update bookmark title.
bookmark.title = form.clean_data['title'] # If the bookmark is being updated, clear old tag list.
if not created:
bookmark.tag_set.clear()
# Create new tag list.
tag_names = form.clean_data['tags'].split()
for tag_name in tag_names:
tag, dummy = Tag.objects.get_or_create(name=tag_name)
bookmark.tag_set.add(tag)
# Save bookmark to database and return it.
bookmark.save()
return bookmark

Now in the same file, replace the code that you removed from bookmark_save_page with a call to _bookmark_save:

@login_required
def bookmark_save_page(request):
if request.method == 'POST':
form = BookmarkSaveForm(request.POST)
if form.is_valid():
bookmark = _bookmark_save(request, form)
return HttpResponseRedirect(
'/user/%s/' % request.user.username
)
else:
form = BookmarkSaveForm()
variables = RequestContext(request, {
'form': form
})
return render_to_response('bookmark_save.html', variables)

The current logic in bookmark_save_page works like this:

if there is POST data:
Validate and save bookmark.
Redirect to user page.
else:
Create an empty form.
Render page.

To implement bookmark editing, we need to slightly modify the logic as follows:

if there is POST data:
Validate and save bookmark.
Redirect to user page.
else if there is a URL in GET data:
Create a form an populate it with the URL's bookmark.
else:
Create an empty form.
Render page.

Let’s translate the above pseudo code into Python. Modify bookmark_save_page in bookmarks/views.py so that it looks like the following (new code is highlighted):

from django.core.exceptions import ObjectDoesNotExist
@login_required
def bookmark_save_page(request):
if request.method == 'POST':
form = BookmarkSaveForm(request.POST)
if form.is_valid():
bookmark = _bookmark_save(request, form)
return HttpResponseRedirect(
'/user/%s/' % request.user.username
)
elif request.GET.has_key('url'):
url = request.GET['url'] title = ''
tags = ''
try:
link = Link.objects.get(url=url)
bookmark = Bookmark.objects.get(
link=link,
user=request.user
)
title = bookmark.title
tags = ' '.join(
tag.name for tag in bookmark.tag_set.all()
)
except ObjectDoesNotExist:
pass
form = BookmarkSaveForm({
'url': url,
'title': title,
'tags': tags
})
else:
form = BookmarkSaveForm()
variables = RequestContext(request, {
'form': form
})
return render_to_response('bookmark_save.html', variables)

This new section of the code first checks whether a GET variable called url exists. If this is the case, it loads the corresponding Link and Bookmark objects of this URL, and binds all the data to a bookmark saving form. You may wonder why we load the Link and Bookmark objects in a try-except construct that silently ignores exceptions. Indeed, it’s perfectly valid to raise an Http404 exception if no bookmark was found for the requested URL. But our code chooses to only populate the URL field in this situation, leaving the title and tags fields empty.

LEAVE A REPLY

Please enter your comment!
Please enter your name here