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.
Thanks for a really interesting post! It's great to see support for django-compress built in!
Thanks. I'd like to see more Django apps which play nicely with each other, I think that is the next step in making the various pluggable apps even more powerful while still obeying Don't Repeat Yourself. That said, it's also an increased burden on developers because the code isn't maintained by the central developers and can break backwards compatibility at any time.
I feel like I have some idea along these lines, but I can't put it together yet. Hmm. :)
I have started to tackle another aspect of the look-and-feel management issue. Specifically this last weekend I created the guts of a template generator that creates Django templates that use the blueprint.css grid scheme for content layout. With it a site developer could easily provide a set of different but related layouts and allow the user to select one in much the same manner as the django-userskins app purports to allow the selection of a userskin for typography, color and such.
The performance and usability tradeoffs are identical for these two cases, so perhaps the "user choice management" portion of your app can be reused for my system.
Rock,
I replied by email, but I'll say it here as well, that sounds like a great project. Maybe there is some room for the projects to cooperate, but regardless feel free to use the code in any way possible.
Will
It's quite a nice approach. I guess it can be used in changing the skin of my blog page.
Thanks for userskins. It is exactly what I was looking for, and I would have had to implement it myself if it wasn't already available.
Just one remark:
In order to user userskins, TEMPLATE_CONTEXT_PROCESSORS must be active, as it uses this way to inject the skin name to the context. An important condition for its activation is using RequestContext in the view, or in other words, when you use render_to_response, don't forget to add the additional argument: context_instance=RequestContext(request)
If you don't do it, you'll get:
TemplateSyntaxError
Caught an exception while rendering: Failed lookup for key [userskins_skin] in ....
Original Traceback (most recent call last): File "/usr/lib/python2.5/site-packages/django/template/debug.py", line 71, in render_node result = node.render(context) [..] skin = template.Variable("userskins_skin").resolve(context) File "/usr/lib/python2.5/site-packages/django/template/init.py", line 676, in resolve value = self._resolve_lookup(context) File "/usr/lib/python2.5/site-packages/django/template/init.py", line 729, in resolvelookup raise VariableDoesNotExist("Failed lookup for key [%s] in %r", (bit, current)) # missing attribute VariableDoesNotExist: Failed lookup for key [userskins_skin] in ..
So now it will be possible to just google for this error message, and not dig in the source code ;-)
Shahar.