Customize site style by user with django-userskins

October 27, 2008. Filed under django 72

For a Django project I'm working on I wanted to allow users to select from a handful of skins to personalize the site's appearance (similar to Twitter's new skinning functionality), and decided to try to put together a pluggable app to accomplish this.

Thus django-userskins was born.

The readme file is fairly thorough on discussing the implementation details, but I'll make a few remarks here nonetheless.

Minimum possible database hits.

I felt that it was important to avoid database hits as much as possible, because allowing users to pick skins isn't much of an improvement if your site starts loading more slowly because of it. My approach is to use cookies for persistence, and if an authenticated user doesn't have a cookie, then it recreates a cookie for them by checking their skin preference in the database. Thus, each user should only have to hit the database once, each time they clean their browser's cookies.

Additionally, django-userskins has an option to never hit the database, and in that case it will only use cookies to store users' preferred skins.

Conserve bandwidth.

Another issue that I felt was important was to conserve bandwidth as much as possible. In this regard, I disliked any approach that involved injecting customized style sheets into application templates because it doesn't allow caching/minification/far-future-expires and is re-sent with every single rendered template.

Instead django-userskins lets you specify the CSS file to load for each skin. A simple case looks like this:

USERSKINS_DEFAULT = "light"
USERSKINS_DETAILS = {
    'light':'light.css',
    'dark':'dark.css',
}

Which will be used by the userskin template tag...

{% load userskins %}
<head>
<title>Yadayada</title>
{% userskin %}
</head>

to render something like this:

<head>
<title>Yadayada</title>
<link rel="stylesheet" href="http://127.0.0.1:8000/media/light.css">
</head>

Even more exciting (to me), is that django-userskins can integrate with django-compress. Your settings file will look something like this:

COMPRESS = True
COMPRESS_VERSION = True
COMPRESS_CSS = {
    'light':{
        'source_filenames':('css/base.css','css/skins/light.css'),
        'output_filename':'css/light_skin.r?.css',
    },
    'dark':{
        'source_filenames':('css/base.css','css/skins/dark.css'),
        'output_filename':'css/dark_skin.r?.css',
    },
}
USERSKINS_DEFAULT = "light"
# USERSKINS_DETAILS is not used when integrating with django-compress
USERSKINS_USE_COMPRESS_GROUPS = True

Usage of the template tag is exactly the same, but will output the compressed, merged, and versioned file for the selected skin.

{% load userskins %}
<head>
<title>Yadayada</title>
{% userskin %}
</head>

Using this combination of django-compress and django-userskins, you can support userskin selection without sending additional data with each page, and (with far-future-expires) without even adding an extra http request to retrieve the skin's css file. Combined with the minimal approach to database access, it should be possible to use django-userskins with minimal impact on your site's performance and bandwidth usage.

Skins for authenticated and anonymous users.

One nice thing about django-userskins is that users who are not logged in can specify a skin as well (persistent via cookies). Code for setting individuals' cookies can be as simple as this (for example, if you just had a list of available skins in a sidebar, and the users just click the link to change their selected skin):

from django.conf.urls.defaults import *
from django.http import HttpResponseRedirect
from userskins.models import SkinPreference

def dark_skin(request):
    hrr = HttpResponseRedirect("/")
    hrr.set_cookie("userskins", "dark")
    if request.user.is_authenticated():
         try:
             sp = SkinPreference.objects.get(user=request.user)
             sp.skin = "dark"
             sp.save()
         except SkinPreference.ObjectDoesNotExist:
             sp = SkinPreference.objects.create(user=request.user,
                                                skin="dark")
    return hrr

def light_skin(request):
    hrr = HttpResponseRedirect("/")
    hrr.set_cookie("userskins", "light")
    if request.user.is_authenticated():
         try:
             sp = SkinPreference.objects.get(user=request.user)
             sp.skin = "dark"
             sp.save()
         except SkinPreference.ObjectDoesNotExist:
             sp = SkinPreference.objects.create(user=request.user,
                                                skin="light")
    return hrr

urlpatterns = patterns(
    '',
    (r'^skin/dark/$', dark_skin),
    (r'^skin/light/$', light_skin),
)

If you don't have authenticated users, then the selection view gets even shorter:

def light_skin(request):
    hrr = HttpResponseRedirect("/")
    hrr.set_cookie("userskins", "light")
    return hrr

Missing anything?

Can you think of any other use cases that are not covered by the existing design? Let me know and I'll incorporate them as best I can.

I'll be integrating django-userskins into a real app of mine sometime soon, and will keep refining it a bit as problems pop up, but I don't imagine making many drastic changes unless new usecases are discovered.

The git repository is on GitHub, and the readme file can be found there as well.