Creating product ratings

One of the innovations in e-commerce web applications has been the use of user-generated content in the form of product reviews and ratings. We've seen some of this in our implementation of customer reviews earlier in the book: users could write short comments about any product in the product catalog. This is too much unstructured information, however. Numeric or star ratings are a more structured alternative that can be provided by users more simply. We can use this as a form of feedback, too, and generate an average rating for all of our products.

Ratings are intended to be quick, easy feedback for customers who wish to contribute, but are not interested in writing a full comment. Many ratings examples exist and it is now a well recognized and even expected idiom for web-based product catalogs.

Using traditional HTTP POST forms, though, does not provide the user experience we would like. It is almost a requirement that we use JavaScript for this type of interaction because users have grown so accustomed to the instant, uninterrupted experience.

In addition to the usability needs of our users, we also need to make sure we capture the rating information in our Django backend. JavaScript and AJAX techniques are the only universal mechanisms of providing this functionality across all browser technologies.

The ratings tool will divide cleanly into two separate aspects: the Django module that lives on the backend and the JavaScript that lives in the browser. We will create our Django module first as a new application in our coleman project, which we will call ratings.

The ratings app will include an extremely simple model that allows us to associate a numeric rating submitted by a user with one of any of our products. In the interest of reuse, we will not construct a ForeignKey directly to our product model, but instead use Django's contenttypes framework to create a GenericForeignKey. This allows us to rate objects other than just products. Perhaps later we'd like to allow users to rate manufacturers or other entities as well.

Our rating model looks like this:

class Rating(models.Model):
    rating = models.IntegerField() 
    content_type = models.ForeignKey(ContentType) 
    object_id = models.PositiveIntegerField() 
    content_object = generic.GenericForeignKey('content_type', 'object_id')

Next we need to build a view through which our JavaScript code can interact with Django, submitting a rating from a user that Django will record as a new Rating object. Django's HttpRequest objects allow us to test whether our view is accessed via AJAX (that is using XMLHttpRequest) or using a standard HTTP request. This method is called is_ajax() and is available on any HttpRequest object.

There are some cases where a view would be designed to handle either an AJAX-style request or an HTTP request. By checking is_ajax, we could branch and return JSON when AJAX is used or render a standard template in the non-AJAX case. For our simple rating view, however, we will only handle AJAX calls and raise a not found message otherwise. The following simple example illustrates this technique:

def myview(request):
    if request.is_ajax():
        # do something ajax-y
        return HttpResponse(...)
    else:
        raise Http404

Design aside: User experience and AJAX

The previous code segment is very simple, but it has an important implication: we are only handling the AJAX case. That means the interface for ratings in our application will only work for users who have JavaScript enabled. This could violate graceful degradation so we must be careful how we implement it in the browser.

This example is intended to illustrate a point about the Web and user experience: it's often difficult to implement advanced functionality in browsers without JavaScript. But it is important to provide these users with an excellent browser experience. Without AJAX, our star-based rating system would require several page loads and an awkward form. In the early days of the Web, this would be commonplace, but on modern sites they're rare and confusing.

Distracting the user with two click-throughs and a form to fill out distances us from the rating tool's original goal: to allow for quick and simple customer feedback. In e-commerce applications, such distractions could be costly. It's a philosophical decision, but in many cases it would seem better to drop the functionality altogether than to wreck the user experience.

An excellent way to design for these user-experience issues is to examine what others do. Netflix and Amazon produce some of the best AJAX-powered interfaces on the Web. Simply disabling JavaScript in your browser and using sites such as these can reveal a whole other world. You'll notice functionality missing that you may be used and sometimes functionality is replaced with a gracefully degraded interface.

Another factor to consider in all of this is development time. If your non-AJAX interface adds twice the development cost and actually hurts the user experience, it is probably a bad business decision. On the other hand, if your functionality is critical and must support all browsers, but you'd like to offer an enhanced version to JavaScript-enabled browsers, then the effort may well pay off.

In fact, this is really what graceful degradation in web interface design should be about and is often called progressive enhancement. First build it without JavaScript or any expectation that it will ever use AJAX. When it's built and working, layer AJAX on top, perhaps by adding an autocomplete field instead of a regular text field or make the form submission asynchronous so that the user's browser does not advance to a new page. Working backwards like this is a great way to ensure your web interfaces work well for all users.

Often you don't care about this or you're building something so complicated that it cannot be done through any other means. As the population of users without JavaScript is relatively small, it is becoming increasingly common to ignore them for complex tools. Be warned, though, that many mobile-enabled browsers have no JavaScript support and if your application is at all targeted toward mobile users, you should ensure to accommodate their entire browser.

This is an increasingly difficult issue, because supporting the whole range of browsers is difficult, costly, and time consuming. When planning a web-based application, this should be a major consideration. It can be difficult to restrict development from the cutting edge just to ensure the small percentage of users without JavaScript can use it. The ramifications of failing to do so will differ across businesses, though, so the decision should be made specifically for one's own application in their own industry.

Product rating view

With these design considerations in mind, we can begin to construct our rating view. Because our aim is to keep the ratings app reusable, our rating view should be written to support ratings for any Django model type. Our rating model does this by using django.contrib.contenttypes and GenericForeignKey.

There are a variety of ways to handle this situation, but almost any solution for the view will also involve the contenttypes framework. We can construct the view in such a way as to include an object_id and content_type parameter. With these two parameters we can obtain the ContentType object and do with it whatever we need. We've seen some of this before and it is a common need in Django applications.

Our ratings app views.py file will appear as follows:

from django.contrib.contenttypes.models import ContentType
from django.core.serializers.json import DjangoJSONEncoder
from django.http import Http404
from django.db.models import get_model
from coleman.ratings.models import Rating


def lookup_object(queryset, object_id=None, slug=None, slug_field=None):
    if object_id is not None:
        obj = queryset.get(pk=object_id) elif slug and slug_field:
        kwargs = {slug_field: slug}
        obj = queryset.get(**kwargs)
    else:
        raise Http404
    return obj

def json_response(response_obj):
    Encoder = DjangoJSONEncoder()
    return HttpResponse(Encoder.encode(response_obj))

def rate_object(request, rating, content_type, object_id):
    if request.is_ajax():
        app_label, model_name = content_type.split('.')
        rating_type = ContentType.objects.get(app_label=app_label,
                                              model=model_name) 
        Model = rating_type.model_class() 
        obj = lookup_object(Model.objects.all(), object_id=object_id) 
        rating = Rating(content_object=obj, rating=rating) 
        rating.save()
        response_dict = {'rating': rating, 'success': True} 
        return json_response(response_dict)
    else:
        raise Http404

The important part is the view itself: rate_object. This view implements the ContentType retrieval we've been talking about and uses it to create a new rating object. The rating view parameter should be a positive or negative integer value, though our application logic could use text values that are converted to appropriate integers if we were interested in writing cleaner URLs.

The json_response helper function takes an object and serializes it to JSON using the built-in DjangoJSONEncoder. This encoder has support for encoding a few additional data types such as dates and decimal values. There is room for improving this function later, by implementing our own custom JSON encoder class, for example, so it's helpful to breakout this code into a small function.

Finally, the lookup_object helper function is something we've seen in earlier chapters. Its job is to retrieve a specific object from a QuerySet given an id or slug value. There are many variations on this function; we could have passed a Model class instead of QuerySet, for example. But the primary goal of all of them is to perform generic lookups.

With our view in place, we can now create rating URLs for all of our Django objects. We can integrate these URLs into a star-rating tool in our templates and ultimately manage their use using JavaScript.

Constructing the template

Creating a rating tool in our template has three components: the tool's image content (stars or dots, and so on), CSS to activate the appropriate rating on hover, and the JavaScript that handles clicks and submits the rating to our Django view. This section will focus on the first two tool elements: the image and CSS.

There are a variety of star rating CSS and image examples on the Web. Some are licensed for reuse in applications, but not all. The technique detailed in this section is a simplified version of the one demonstrated by Rogie at Komodo Media at the following URL: http://www.komodomedia.com/blog/2005/08/creating-a-star-rater-using-css/. Rogie has written about other approaches to this problem, including some with better cross-browser support. See the tutorials at komodomedia.com for additional information.

We have created the stars images for this book using a simple graphics editor and the star character from the Mac OS X webdings font. The star has two states: inactive and active. Thus we need two images, one for each state. The star images are shown below:

Constructing the template

With our stars created, we can begin developing a set of stylesheet rules that reveals our creation. The CSS rules need to decorate <a> tags so that we have a hook for our JavaScript code later. Let's start with the following basic HTML, which we will enhance as we go:

<a href="">Rate 1 Star</a>
<a href="">Rate 2 Stars</a>
<a href="">Rate 3 Stars</a>
<a href="">Rate 4 Stars</a>

Notice our href attribute is not yet filled in. We will be using Django's {% url %} template tag to handle this URL, but for clarity it will be omitted until later. The next step is to add class declarations for each of the four types of rating actions:

<a class="one" href="">Rate 1 Star</a>
<a class="two" href="">Rate 2 Stars</a>
<a class="three" href="">Rate 3 Stars</a>
<a class="four" href="">Rate 4 Stars</a>

Adding CSS classes to our <a> tags gives us a place to begin when styling our star rating tool. We will also want to wrap the whole set of rating links in a <ul> tag and each individual link in <li>:

<ul class="rating-tool">
   <li><a class="one" href="">Rate 1 Star</a></li>
   <li><a class="two" href="">Rate 2 Stars</a></li>
   <li><a class="three" href="">Rate 3 Stars</a></li>
   <li><a class="four" href="">Rate 4 Stars</a></li>
</ul>

Wrapping the links as an unordered list lets us make sure the links are displayed horizontally, as a set of side-by-side stars.

Now that we have our HTML ready, we can begin writing CSS rules. The first rule applies to our <ul> class. It will let us define the size of our rating tool and sets the background to our inactive star image.

.rating-tool {
    list-style-type: none;
    width: 165px;
    height:50px;
    position: relative;
    background: url('/site_media/img/stars.png') top left repeat-x;
}

Because our star image is 40 pixels wide and 50 pixels tall, the size of our rating tool, which will support four stars, is set to 165 pixels wide.

Next we style the <li> elements so that they align horizontally instead of vertically. There are several ways of achieving this, but here we will use the float: left rule:

.rating-tool li {
margin:0; padding: 0;
float: left;
}

With our <li> tags style appropriately, we can start styling the <a> tags themselves. These tags are the crux of our rating tool. We must ensure several things to make this work: that the link text is hidden, that each <a> width is set to match the width of an individual star, and that we position the <a> completely within our rating-tool <ul>. We will also set the z-index rule, which will function as an image mask:

.rating-tool li a {
    display: block;
    width: 50px; 
    height: 50px;
    text-indent: -10000px;
    text-decoration: none;
    position: absolute;
    padding:0;
    z-index:5;
}

Each <a> also defines a :hover pseudoclass. When the link is hovered, the background is changed using a CSS image replacement technique and the z-index mask is adjusted to show only the portion of stars currently hovering. In other words, when the user hovers over the second star, they should see both the first and second stars activate. This is completed with the help of the next set of rules:

.rating-tool li a:hover { 
    background: url('/site_media/img/stars.png') left bottom; 
    z-index: 1; 
    left:0px;
}

Each anchor tag should define a new width for itself specific to which star we're revealing. The fourth star is going to reveal the entire rating tool by setting a width of 200 pixels when hovered. These rules work in conjunction with the previous, more general set of rules.

.rating-tool a.one { left: 0px; }
.rating-tool a.one:hover { width: 50px; }
.rating-tool a.two { left: 50px; }
.rating-tool a.two:hover { width: 100px; }
.rating-tool a.three { left: 100px; }
.rating-tool a.three:hover { width: 150px; }
.rating-tool a.four { left: 150px; }
.rating-tool a.four:hover { width: 200px; }

This finishes the CSS rules for our rating tool. You can see the results of this design in the following screenshots:

Constructing the template

The activation state of the rating tool reveals yellow stars when the mouse hovers over the rating control, as in the following screenshot:

Constructing the template

This is just one of many possible techniques for implementing a rating tool. It works well because it uses a pure combination of HTML and CSS for the graphics effect.

One last step is required before we can begin work on the JavaScript code that will power our rating tool. Earlier we mentioned that we purposefully excluded the href attribute for our <a> tags. We now need to wire these up to our Django view.

To do this we will use Django's built-in {% url %} tag and a named URL pattern. Our rating view from the previous section lives in coleman.ratings.views. Our urlpatterns will then look like this:

urlpatterns = patterns(
    '',
    url(
      r'^(?P<content_type>[^/]+)/(?P<object_id>d+)/(?P<rating>[1-5])/$',
      'coleman.ratings.views.rate_object',
      name='rate_object'),
 )

Using the url() function, we can create a named URL pattern, which we've called rate_object. Using a named URL pattern means we can reference it easily from templates using {% url %}.

The {% url %} template tag takes a path to a view or the name of a URL pattern. For example, we could have written:

{% url coleman.ratings.views.rate_object ... %}

But when a view could potentially be used in multiple URL patterns, it's easier to use a named URL:

{% url rate_object ... %}

Using the {% url %} tag is an important habit because it prevents the hard coding of absolute URLs into HTML templates. This will save tremendous amounts of time if we ever decide to change our view or change the layout of URLs on our site. Instead of manually having to rewrite each href attribute that linked to the changed URL, Django will do it for us automatically.

The {% url %} tag also takes arguments to be passed on the URL line and will construct the appropriate URL using these arguments. For example, our rate_object view takes three arguments: content_type, object_id, and rating. These correspond directly to our view function arguments. A rating URL may look like this:

/rating/products.Product/125/4/

This translates to giving a four rating to the Product with the primary key 125. We could hard-code this in using template variables, but instead we'll use the {% url %} tag. Assume that the template receives a template variable named object that contains the Product we will be rating. An equivalent {% url %} tag will look like this:

{% url rate_object content_type='products.Product' object_id=object.id rating=4 %}

Notice how we are still able to reference template variables, like object, in the tag.

Now that we have a working {% url %} tag, we can integrate into the HTML for our rating tool. Each star will correspond to a rating with a matching value from one to four. The Django template code will look like this:

<ul class="rating-tool">
  <li>
   <a class="one" 
     href="{% url rate_object content_type='products.Product' 
                    object_id=object.id rating=1
                %}">Rate 1 Star</a>
  </li>
  <li>
   <a class="two" 
     href="{% url rate_object content_type='products.Product' 
                   object_id=object.id rating=2
                %}">Rate 2 Stars</a>
   </li>
   <li>
     <a class="three" 
       href="{% url rate_object content_type='products.Product' 
                     object_id=object.id rating=3
                  %}">Rate 3 Stars</a>
   </li>
   <li>
       <a class="four" 
         href="{% url rate_object content_type='products.Product' 
                        object_id=object.id rating=4
                    %}">Rate 4 Stars</a>
   </li>
</ul>

Writing the JavaScript

We will build our JavaScript rating tool using YAHOO's YUI JavaScript utilities discussed earlier in the chapter. These utilities simplify the code considerably and insure against cross-browser compatibility problems.

In order to take advantage of the YUI library, we have to embed it in our templates using a <script> like normal JavaScript code. The following <script> will bundle all of the YUI utilities we'll be working with:

<script type="text/javascript" src="http://yui.yahooapis.com/combo?2.8.0r4/build/yahoo-dom-event/yahoo-dom-event.js&2.8.0r4/build/connection/connection_core-min.js&2.8.0r4/build/json/json-min.js"></script>

That long string of a URL in the src attribute is generated using YUI's excellent dependency configurator. This will automatically choose the most efficient script tag to embed in your application based on the YUI modules you are using. You can access the configurator via: http://developer.yahoo.com/yui/articles/hosting/.

Our JavaScript code is divisible into three parts: the main routine, which listens for the browser's ready event and kicks off our other code, a function to set up our click listeners, and a handler routine that takes action when a click is detected.

The main routine is very simple and is run as soon as the browser loads our JavaScript file from the server. This should happen late in the page load process and we can take an extra step to encourage this by embedding our <script> tag at the end of our template's <body>.

When the script loads, we will instruct YUI to set up another listener. This time we'll be listening for a DOMReady event. This is one of the ways YUI helps us: the DOMReady event is a YUI construct that will fire when our page's DOM has stabilized. This means the elements have rendered and the HTML has been fully parsed. This is part of YUI's Event utility package and we use it like this:

YAHOO.util.Event.onDOMReady(init);

The init part is a function we will write called init. When the YUI tool has detected the DOM is stable, this code will call init and execute its function body. Let's write the init routine now:

function init(e) { 
    var rating_tool = document.getElementById('rating'), 
    rating_links = rating_tool.getElementsByTagName('a'), 

    for (var i=0; i < rating_links.length; i++) {
         YAHOO.util.Event.addListener(rating_links[i], 'click', handleClick, i+1);
    }
 }

The init function gathers up the <a> elements on our pages that are used for our rating tool. It does so by finding the <ul> tag, to which we've added the ID attribute rating. It then loads all the <a> elements within the rating tool's <ul> tag and begins to process them.

As the code loops through all of our <a> ratings, it attaches to each one an event listener. These listeners all employ the same function: handleClick. It also includes the counter variable i, which we use to keep track of which stars were clicked.

At this point code execution stops. Nothing else happens during the page load process and our user can go about their business. Eventually, they will find a product they like and attempt to give it a rating. When they click on the <a> tag corresponding to their star rating, it will activate the handler we set up in the init function.

These click handlers are where the magic happens. They need to do several things. First, they prevent the browser from taking the usual action when you click on an <a>. This is typically to load the next page, linked to via the href attribute. If we do not prevent the browser from doing this, we'll transport the user to a possibly broken URL or otherwise break our JavaScript.

YUI will automatically provide the handleClick function with its first argument. This is an event object that contains information about what event has fired and what object it has fired on. It is a very useful object and we will need it shortly.

It is possible to prevent default behaviors using pure JavaScript, but the YUI routines perform better across all browsers. Events and event handling is one area where browsers can differ greatly. The YUI Event utility provides a preventDefault function that we can use to stop the browser from performing its usual action:

YAHOO.util.Event.preventDefault(e);

In the above code, e is the event object YUI has passed to our handler function. This call to preventDefault on the event object will cease the browser's default behavior as soon as our handler completes execution.

The next task our handler needs to perform is to submit the rating to our Django backend. This will finally link the frontend to the backend code we wrote earlier. We want to submit this information asynchronously. This means the browser will send off the request to our Django rate_object view and then go about its business, allowing the user to continue to interact with our application.

When Django has finished processing the request, the browser will be notified and an event is fired. This event is handled by code we set up at the time of the asynchronous call. We will use YUI's Connection Manager to perform this operation, which greatly simplifies this process.

First, we need to define our callback handler. YUI's Connection tool uses a JavaScript object with success and failure properties. These properties are functions that will run in the case of a successful response from the server or an unsuccessful one, respectively. Unsuccessful asynchronous calls are the result of the standard kinds of HTTP failure conditions: 404 and 500 errors, especially.

The callback handler object will look like this:

callback = {
    success: function(o){
        var result = YAHOO.lang.JSON.parse(o.responseText); 
        if (result.success == true) { 
             alert("You've rated this object " + result.rating);
        }
     },
    failure: function(o){
         alert("Failed to rate object!");
    }
 };

Note that we've taken a very simplified approach here, notifying the user of their rating via alert(). A production setup would likely want to take additional steps to update the UI to indicate a rating had been made, possibly by modifying the CSS rules we detailed earlier.

There are a couple of other things going on as well. First, both the success and failure functions receive an object o. This object contains information about the asynchronous call. To access the raw server response in either case, for example, you can use the o.responseText property.

Our success function employs another YUI tool, the JSON utility. This includes a safe JSON parser. Our o.responseText will contain JSON data but will contain it as a raw JavaScript string. One way to convert this JSON to actual JavaScript data objects is with eval. But eval can be dangerous and YUI's JSON utility provides us with a safe parse function to evaluate the server's response.

Once evaluated, the result variable contains a standard JavaScript object with properties translated from the JSON sent back from the server. We can use this as we would any JavaScript data.

With our callback handler in place, we can return to the Connection Manager call we began to make a few paragraphs ago. This call needs two other pieces of information: the HTTP method to use and the URL to send our asynchronous call. The method is simply the usual HTTP GET or POST. Our Django view has no particular method requirement so we will use POST.

The URL part is more difficult. Remember when we developed our Django template we included the rating URL using the {% url %} template tag. We can now extract that URL and use it with our connection manager function.

To access the appropriate URL, we again use our event object provided by the event utility. Using the YUI event module's getTarget function, we can obtain the <a> tag that was clicked. This <a> tag will contain the URL we need in its href attribute:

var target = YAHOO.util.Event.getTarget(e)

We can access the href via the getAttribute DOM method:

var url = target.getAttribute('href')

We're now ready to send off our asynchronous request, by calling the YUI Connection Manager's asyncRequest function:

YAHOO.util.Connect.asyncRequest('POST', url, callback)

The complete JavaScript routine for our rating tool appears as follows:

(function(){
    function handleClick(e, count) {
        var target = YAHOO.util.Event.getTarget(e),
        url = target.getAttribute('href'),
        callback = {
           success: function(o){
              var result = YAHOO.lang.JSON.parse(o.responseText); 
              if (result.success == true) { 
                alert("You've rated this object " + result.rating);
            }
          },
          failure: function(o){
               alert("Failed to rate object!");
       }
    };

   YAHOO.util.Event.preventDefault(e);
   YAHOO.util.Connect.asyncRequest('POST', url, callback);
   };

  function init(e) { 
       var rating_tool = document.getElementById('rating'), 
       rating_links = rating_tool.getElementsByTagName('a'), 
       for (var i=0; i < rating_links.length; i++) {       
            YAHOO.util.Event.addListener(rating_links[i], 'click', handleClick, i+1);
     }
   }
YAHOO.util.Event.onDOMReady(init);
}());

Debugging JavaScript

JavaScript is notoriously difficult to debug. In our rating tool example there are many potential failure points. Primary among them is what happens when Django fails to return a correct response. We will be notified of this case because of the failure function in our callback object.

But how do we debug the problem? There is an increasing amount of tools available to perform debugging of this sort. Two of the simplest and easiest to get installed are FireBug, for FireFox, and Safari's Web Developer functions, which now come with Safari 4.4 and Google Chrome.

These tools let you inspect the HTML DOM, see the JavaScript error console, and see the results of XHR calls, such as those made by asyncRequest. Any error page that Django generates as a result of an AJAX call will be returned to these web development debuggers in the resources section and can be inspected to determine what exception Django has thrown and where it occurred in our view code.

Another common practice is to insert debugging alert() statements at critical points in the JavaScript code. This is not an elegant method, and one has to be sure to remove any debugging statements when pushing code to production use, but it is quick and effective.

If using a JavaScript debugging tool such as Firebug (see above), another popular debugging technique involves the console global object. This is a global variable inserted by debugging tools to allow advanced debugging functionality. It can be used similarly to alert statements by calling console.log() and passing a string to log as debugging output. There are many additional functions in the console API, though specific features may or may not be available depending on the browser or debugging tool in use.

In addition, most JavaScript frameworks include their own set of tools for debugging applications. See the documentation for your framework of choice to get more information.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset