Tinkering Around in Django JavaScript Integration

0
93
8 min read

 

Django JavaScript Integration: AJAX and jQuery

Django JavaScript Integration: AJAX and jQuery

Develop AJAX applications using Django and jQuery

  • Learn how Django + jQuery = AJAX
  • Integrate your AJAX application with Django on the server side and jQuery on the client side
  • Learn how to handle AJAX requests with jQuery
  • Compare the pros and cons of client-side search with JavaScript and initializing a search on the server side via AJAX
  • Handle login and authentication via Django-based AJAX
        Read more about this book      

(For more resources on this subject, see here.)

Minor tweaks and bugfixes

Good tinkering can be a process that begins with tweaks and bugfixes, and snowballs from there. Let’s begin with some of the smaller tweaks and bugfixes before tinkering further.

Setting a default name of “(Insert name here)”

Most of the fields on an Entity default to blank, which is in general appropriate. However, this means that there is a zero-width link for any search result which has not had a name set. If a user fills out the Entity’s name before navigating away from that page, everything is fine, but it is a very suspicious assumption that all users will magically use our software in whatever fashion would be most convenient for our implementation.

So, instead, we set a default name of “(Insert name here)” in the definition of an Entity, in models.py:

name = models.TextField(blank = True,
default = u'(Insert name here)’)

Eliminating Borg behavior

One variant on the classic Singleton pattern in Gang of Four is the Borg pattern, where arbitrarily many instances of a Borg class may exist, but they share the same dictionary, so that if you set an attribute on one of them, you set the attribute on all of them. At present we have a bug, which is that our views pull all available instances. We need to specify something different. We update the end of ajax_profile(), including a slot for time zones to be used later in this article, to:

return render_to_response(u’profile_internal.html’,
{
u’entities’: directory.models.Entity.objects.filter(
is_invisible = False).order_by(u’name’),
u’entity’: entity,
u’first_stati’: directory.models.Status.objects.filter(
entity = id).order_by(
u’-datetime’)[:directory.settings.INITIAL_STATI],
u’gps’: gps,
u’gps_url’: gps_url,
u’id’: int(id),
u’emails’: directory.models.Email.objects.filter(
entity = entity, is_invisible = False),
u’phones’: directory.models.Phone.objects.filter(
entity = entity, is_invisible = False),
u’second_stati’: directory.models.Status.objects.filter(
entity = id).order_by(
u’-datetime’)[directory.settings.INITIAL_STATI:],
u’tags’: directory.models.Tag.objects.filter(entity = entity,
is_invisible = False).order_by(u’text’),
u’time_zones’: directory.models.TIME_ZONE_CHOICES,
u’urls’: directory.models.URL.objects.filter(entity = entity,
is_invisible = False),
})

Likewise, we update homepage():

profile = template.render(Context(
{
u’entities’:
directory.models.Entity.objects.filter(
is_invisible = False),
u’entity’: entity,
u’first_stati’: directory.models.Status.objects.filter(
entity = id).order_by(
u’-datetime’)[:directory.settings.INITIAL_STATI],
u’gps’: gps,
u’gps_url’: gps_url,
u’id’: int(id),
u’emails’: directory.models.Email.objects.filter(
entity = entity, is_invisible = False),
u’phones’: directory.models.Phone.objects.filter(
entity = entity, is_invisible = False),
u’query’: urllib.quote(query),
u’second_stati’:directory.models.Status.objects.filter(
entity = id).order_by(
u’-datetime’)[directory.settings.INITIAL_STATI:],
u’time_zones’: directory.models.TIME_ZONE_CHOICES,
u’tags’: directory.models.Tag.objects.filter(
entity = entity,
is_invisible = False).order_by(u’text’),
u’urls’: directory.models.URL.objects.filter(
entity = entity, is_invisible = False),
}))

Confusing jQuery’s load() with html()

If we have failed to load a profile in the main search.html template, we had a call to load(“”). What we needed was:

else
{
$(“#profile”).html(“”);
}

$(“#profile”).load(“”) loads a copy of the current page into the div named profile. We can improve on this slightly to “blank” contents that include the default header:

else
{
$(“#profile”).html(“<h2>People, etc.</h2>”);
}

Preventing display of deleted instances

In our system, enabling undo means that there can be instances (Entities, Emails, URLs, and so on) which have been deleted but are still available for undo. We have implemented deletion by setting an is_invisible flag to True, and we also need to check before displaying to avoid puzzling behavior like a user deleting an Entity, being told Your change has been saved, and then seeing the Entity’s profile displayed exactly as before.

We accomplish this by a specifying, for a Queryset .filter(is_invisible = False) where we might earlier have specified .all(), or adding is_invisible = False to the conditions of a pre-existing filter; for instance:

def ajax_download_model(request, model):
if directory.settings.SHOULD_DOWNLOAD_DIRECTORY:
json_serializer = serializers.get_serializer(u’json’)()
response = HttpResponse(mimetype = u’application/json’)
if model == u’Entity’:
json_serializer.serialize(getattr(directory.models,
model).objects.filter(
is_invisible = False).order_by(u’name’),
ensure_ascii = False, stream = response)
else:
json_serializer.serialize(getattr(directory.models,
model).objects.filter(is_invisible = False),
ensure_ascii = False,
stream = response)
return response
else:
return HttpResponse(u’This feature has been turned off.’)

In the main view for the profile, we add a check in the beginning so that a (basically) blank result page is shown:

def ajax_profile(request, id):
entity = directory.models.Entity.objects.filter(id = int(id))[0] if entity.is_invisible:
return HttpResponse(u'<h2>People, etc.</h2>’)

One nicety we provide is usually loading a profile on mouseover for its area of the search result page. This means that users can more quickly and easily scan through drilldown pages in search of the right match; however, there is a performance gotcha for simply specifying an onmouseover handler. If you specify an onmouseover for a containing div, you may get a separate event call for every time the user hovers over an element contained in the div, easily getting 3+ calls if a user moves the mouse over to the link. That could be annoying to people on a VPN connection if it means that they are getting the network hits for numerous needless profile loads.

To cut back on this, we define an initially null variable for the last profile moused over:

Then we call the following function in the containing div element’s onmouseover:

PHOTO_DIRECTORY.last_mouseover_profile = null;

Then we call the following function in the containing div element’s onmouseover:

PHOTO_DIRECTORY.mouseover_profile = function(profile)
{
if (profile != PHOTO_DIRECTORY.last_mouseover_profile)
{
PHOTO_DIRECTORY.load_profile(profile);
PHOTO_DIRECTORY.last_mouseover_profile = profile;
PHOTO_DIRECTORY.register_editables();
}
}

The relevant code from search_internal.html is as follows:

<div class=”search_result”
onmouseover=”PHOTO_DIRECTORY.mouseover_profile(
{{ result.id }});”
onclick=”PHOTO_DIRECTORY.click_profile({{ result.id }});”>

We usually, but not always, enable this mouseover functionality; not always, because it works out to annoying behavior if a person is trying to edit, does a drag select, mouses over the profile area, and reloads a fresh, non-edited profile. Here we edit the Jeditable plugin’s source code and add a few lines; we also perform a second check for if the user is logged in, and offer a login form if so:

/* if element is empty add something clickable
(if requested) */
if (!$.trim($(this).html())) {
$(this).html(settings.placeholder);
}
$(this).bind(settings.event, function(e) {

$(“div”).removeAttr(“onmouseover”);
if (!PHOTO_DIRECTORY.check_login())
{
PHOTO_DIRECTORY.offer_login();
}
/* abort if disabled for this element */
if (true === $(this).data(‘disabled.editable’)) {
return;
}

For Jeditable-enabled elements, we can override the placeholder for an empty element at method call, but the default placeholder is cleared when editing begins; overridden placeholders aren’t. We override the placeholder with something that gives us a little more control and styling freedom:

// publicly accessible defaults
$.fn.editable.defaults = {
name : ‘value’,
id : ‘id’,
type : ‘text’,
width : ‘auto’,
height : ‘auto’,
event : ‘click.editable’,
onblur : ‘cancel’,
loadtype : ‘GET’,
loadtext : ‘Loading…’,
placeholder:'<span class=”placeholder”>
Click to add.</span>’,
loaddata : {},
submitdata : {},
ajaxoptions: {}
};

All of this is added to the file jquery.jeditable.js.

We now have, as well as an @ajax_login_required decorator, an @ajax_permission_required decorator. We test for this variable in the default postprocessor specified in $.ajaxSetup() for the complete handler. Because Jeditable will place the returned data inline, we also refresh the profile.

This occurs after the code to check for an undoable edit and offer an undo option to the user.

complete: function(XMLHttpRequest, textStatus)
{
var data = XMLHttpRequest.responseText;
var regular_expression = new RegExp(“<!-” +
“-# (d+) #-” + “->”);
if (data.match(regular_expression))
{
var match = regular_expression.exec(data);
PHOTO_DIRECTORY.undo_notification(
“Your changes have been saved. ” +
“<a href=’JavaScript:PHOTO_DIRECTORY.undo(” +
match[1] + “)’>Undo</a>”);
}
else if (data == ‘{“not_permitted”: true}’ ||
data == “{‘not_permitted’: true}”)
{
PHOTO_DIRECTORY.send_notification(
“We are sorry, but we cannot allow you ” +
“to do that.”);
PHOTO_DIRECTORY.reload_profile();
}
},

Note that we have tried to produce the least painful of clear message we can: we avoid both saying “You shouldn’t be doing that,” and a terse, “bad movie computer”-style message of “Access denied” or “Permission denied.”

We also removed from that method code to call offer_login() if a call came back not authenticated. This looked good on paper, but our code was making Ajax calls soon enough that the user would get an immediate, unprovoked, modal login dialog on loading the page.

Adding a favicon.ico

In terms of minor tweaks, some visually distinct favicon.ico (http://softpedia.com/ is one of many free sources of favicon.ico files, or the favicon generator at http://tools.dynamicdrive.com/favicon/ which can take an image like your company logo as the basis for an icon) helps your tabs look different at a glance from other tabs. Save a good, simple favicon in static/favicon.ico. The icon may not show up immediately when you refresh, but a good favicon makes it slightly easier for visitors to manage your pages among others that they have to deal with. It shows up in the address bar, bookmarks, and possibly other places.

This brings us to the end of the minor tweaks; let us look at two slightly larger additions to the directory.

LEAVE A REPLY

Please enter your comment!
Please enter your name here