I’ve been making some great progress on the shoutbox. I’ve added smilies and I used a jQuery plugin to get the edit-in-place working. It’s awesome! Meanwhile, while it is fresh in my mind, I’ll post about the smilies here.
In case you missed the earlier parts, here they are: part 1, part 2, part 3.
One of the “fun” features of the shoutbox is the smilies, or emoticons. In a nutshell, certain character sequences are translated into small images in the “shout” text. Furthermore, all of the images are displayed below the shoutbox, and by clicking on them they get added to the shout the user is typing via some simple javascript.
I can see myself using this smiley function in lots of places: shoutbox, user comments, photo of the day, forums, etc. So I created a new application for it, separate from the shoutbox application.
I read up on django’s custom filter documentation, and spent a fair amount of time scratching my head over the auto-escaping feature. It suddenly occured to me that what I need is similar to the django urlize filter: go through some text and replace certain phrases with HTML. In the urlize case this is an <a> tag. In my case this is an <img> tag. I then studied the urlize filter source to steer me. Mine turned out a bit simpler as you will see below.
First let’s start with the model. I wanted to be able to easily edit and add smilies via the admin interface. The legacy shoutbox allowed many “phrases” to map to a single smiley, but it did this in a redundant way. I decided to keep things simple and only allow one phrase per smiley. Part of this was driven by the fact that if the user clicks on a smiley I need to add the phrase to the text they are typing. If there are multiple phrases, which do I use? I suppose I could have picked the first one, but this was going against my “let’s keep this simple for now” philosophy. Here is the model I came up with:
class Smiley(models.Model):
image = models.ImageField(upload_to='smiley/images/')
title = models.CharField(max_length=32)
code = models.CharField(max_length=32)
objects = SmileyManager()
class Meta:
verbose_name_plural = 'Smilies'
ordering = ('title', )
def __unicode__(self):
return self.title
def get_absolute_url(self):
return self.image.url
def html(self):
if self.image:
return u'<img src="%s" alt="%s" title="%s" />' % \
(self.get_absolute_url(), self.title, self.title)
return u''
html.allow_tags = True
This is pretty straightforward. I’ll explain the custom manager in a bit. I wanted to be able to show the smiley image in the admin section, so I added a html() member function to the Smiley class. This would be useful in templates also. In order to get the HTML to display correctly in the admin, I needed to add an “allow_tags” attribute set to True to the html() function. I can then use the html() function in the admin class in my admin.py file:
from django.contrib import admin
from smiley.models import Smiley
class SmileyAdmin(admin.ModelAdmin):
list_display = ('title', 'code', 'html')
admin.site.register(Smiley, SmileyAdmin)
And with just that little bit of code I now have a nice admin interface to add smilies.
I then turned my attention to creating a custom filter for “smilifying” text. Again, I studied the urlize filter to see what was being done there (splitting the text into words), but steered my code towards the example in the documentation on how to handle auto-escaping. I realized that I could be hitting the database a lot to read the smiley definitions trying to smilify text, so I decided to cache the smiley information. A convenient way to do this was to add a custom Smiley model manager. I knew I needed a dictionary that mapped smiley code phrases to <img> HTML. And I probably would need to cache the smiley objects themselves. I handled this in a custom model manager (in models.py):
class SmileyManager(models.Manager):
smiley_map = None
smilies = None
def get_smiley_map(self):
if self.smiley_map is None:
smilies = self.all()
self.smiley_map = {}
for s in smilies:
self.smiley_map[s.code] = s.html()
return self.smiley_map
def get_smilies(self):
if self.smilies is None:
self.smilies = self.all()
return self.smilies
def clear_cache(self):
self.smiley_map = None
self.smilies = None
The get_smiley_map() and get_smilies() use caching techniques to minimize database hits. I added a clear_cache() function to force a re-read of the database. I envision that if I ever need to add or delete a smiley from the admin interface I will need to add a custom admin view that calls clear_cache(). We’ll leave that as a hook for now and cross that bridge later. Now my filter code can simply call the Smiley objects manager to get the “smiley map”.
Here is my new filter called, of course, “smilify”. I put this in the templatetags directory in a file called smiley_tags.py:
import re
from django import template
from django.template.defaultfilters import stringfilter
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
from smiley.models import Smiley
register = template.Library()
word_split_re = re.compile(r'(\s+)')
@register.filter
@stringfilter
def smilify(value, autoescape=False):
"""A filter to "smilify" text by replacing text with HTML img tags of smilies."""
if autoescape:
esc = conditional_escape
else:
esc = lambda x: x
smiley_map = Smiley.objects.get_smiley_map()
words = word_split_re.split(value)
for i, word in enumerate(words):
if word in smiley_map:
words[i] = smiley_map[word]
else:
words[i] = esc(words[i])
return mark_safe(u''.join(words))
smilify.needs_autoescape = True
The basic idea is to split the text into words. If a word is in our smiley map, substitute it with the corresponding <img> tag. Otherwise we need to escape the word. At the end we join the words together and mark the resulting string safe.
I then updated my shoutbox template to use this new filter and voila we had smilies!
Finally I needed a way to present all of the smileys to the user. These smilies would be clickable, and doing so would add the code for that smiley to whatever input box the comment or shout would be using. I created an inclusion tag called smiley_farm in my smiley_tags.py file to handle this. It uses the Smiley manager’s get_smilies functions, which caches the smiley objects:
@register.inclusion_tag('smiley/smiley_farm.html')
def smiley_farm():
"""An inclusion tag that displays all of the smilies in clickable form."""
return {'smilies': Smiley.objects.get_smilies(), }
The template smiley_farm.html looks like this:
<div class="smiley_farm">
{% for s in smilies %}
<img src="{{ s.image.url }}" alt="{{ s.code }}" title="{{ s.title }} {{ s.code }}"
onclick="sb_smiley_click(' {{ s.code }} ');" />
{% endfor %}
</div>
I enclose the smilies in a div so I can easily style them with CSS. In particular I added a cursor: pointer style to the images so that the mouse pointer will indicate to the user that the smilies can be clicked. When you click on a smiley, the sb_smiley_click() javascript function will run, which simply adds the smiley code phrase to an input text with a certain id:
function sb_smiley_click(code)
{
var txt = document.getElementById("shoutbox-smiley-input");
txt.value += code;
txt.focus();
}
As I learn more about jQuery I hope to revisit this code and add the onclick function to the images after the page has loaded.
Finally, I added a bit of javascript to hide the smiley farm until the user clicks a button labeled “Smilies” to reveal them to conserve screen real estate. This idea and behavior is copied from the legacy shoutbox.
That was a long post but with these pretty simple pieces in place I now have smilies for the shoutbox and other applications to use!
In the next post I’ll show how I got the edit-in-place functionality working.
[...] In this post I’ll show how I got an edit-in-place feature working with the shoutbox. In case you missed the earlier parts, here they are: part 1, part 2, part 3, part 4. [...]
(Sorry I pressed submit too early last time….)
Thanks for the excellent write-up on the shoutbox and smilify filter! I had a shoutbox up and running in no-time.
Since I’m showing the shoutbox on nearly every page I set out to optimize the Manager a bit to avoid the two extra database hits for the shoutbox and smilies.
It’s very easy using Django’s build-in caching mechanism:
Import the caching system:
from django.core.cache import cache
Add a cache key and initilize the smiley_map from cache:
cache_key = ’smileymap’
smiley_map = cache.get(cache_key)
smiley_map will still be None if it isn’t in the cache, so set the cache in get_smiley_map(self) after populating it:
cache.set(self.cache_key, self.smiley_map, 24*60*60)
And you’re done..
Clear the cache like this if the smileys change:
cache.delete(self.cache_key)
Thanks for the comment! The way it is coded now, in the post, is that the smiley_map is already persistent, so you don’t need to cache it. It only hits the database the first time it is asked for. However I was thinking about switching over to using the cache, since that may be more thread safe.
Since writing this post I have changed my shoutbox a bit. I am now using AJAX to post shouts and AJAX to get the smilies (to avoid loading them until the user actually wants them). Perhaps I will write a short note about this in the near future.
That’s weird, I was seeing Django hitting the database once on every request to get the smilies. Utilizing the cache got rid of it. I’ve applied the same caching to the shoutbox as well, eliminating another database hit.
I also added some ajax magic to the shoutbox to post and delete comments using request.is_ajax() to return a different response in the view.
Have you considered putting the shoutbox and/or smilify code up on code.google.com? I reckon it would be usefull to other people as well and would allow for people to exchange patches or code.
Anyway, thanks again for the great posts, looking forward to more of them.
Sorry, I think I misunderstood you there. The smiley_map is a singleton, and only hits the database once to get the smilies. I’ve got the SQL printed out in my template and I see it only on the first page load after starting the server.
Since this post was made I have also changed the shoutbox to use AJAX, loading the smilies only on demand, and for posting shouts. I also got rid of the scrolling.
My code is currently available at http://code.surfguitar101.com.
That’s a good idea about using is_ajax and returning a different response.
Thanks again!