Hiatus 1
Thoughts on the Django Sprint 0
The Django sprint took place on December 1st, and while I couldn’t commit too much time I did manage to add a patch for one ticket, for making 403 pages customizable instead of being hard-coded.
It wasn’t an extremely difficult ticket since there were ample examples of how 404 and 500 pages enabled customization. I paired with my colleague Ionut on the ticket and we went about it pretty comprehensively imho, with tests for our changes, and also added tests for the CSRF middleware (previously there were none).
One minor issue with the sprint was the server outage for the Django Trac site that lasted a few hours. Surely someone had to know about this before hand (the hosting company gave 10 days notice) and could have done anything from simply posting about the outage on the sprint wiki page to making arrangements for the page to be mirrored.
Also it might have been more efficient to divvy out tickets before the sprint itself, instead of a “come and grab what you’d like to do” approach. I understand that the core devs don’t want to be too heavy-handed in managing the project, but I think a worldwide sprint could deal with a tad more organization.
These minor grievances aside I think the sprint went really well. Having events like this where everyone gets together (virtually or physically) is a good way to bring new developers into a project. At my office, we had several participants unfamiliar with Django itself, let alone Django internals. But by the end of the sprint they left with some knowledge of Django, and the entire workflow behind contributing to it.
Using database views with Django models 0
I’m surprised this feature isn’t promoted more (maybe because it involves raw SQL?). You can hook a Django model to a database view. It can come in very useful when you’re trying to aggregate common information from various models.
Problem to solve
Say I have the following models: Album, Artist, and Dvd on a music site that accepts submissions. Before submissions are shown on the site they must be approved. So each of them has a boolean saying whether they are approved or not, something like:
approved = models.BooleanField(default=False)
Let’s complicate the situation a little to make for a better example. Say, the equivalent field for the Dvd model is called “not_unapproved1”. Also, Albums and Dvds have a foreign key to record label2, something like:
record_label = models.ForeignKey(EvilRIAAMember)
Our goal is to have a model called Unapproved which aggregates the Album, Artist, Dvd objects that have not been approved. It should provide access to the ‘approved’ and ‘record_label’ fields. Then I can placate my site editors who are clamoring for an easy way to find all unapproved content regardless of content type.
Step 1: View definition
UNAPPROVED_VIEW = """
CREATE OR REPLACE VIEW unapproved_view AS
SELECT
(SELECT nextval('unapproved_sequence')) as id,
o.id AS object_id,
ct.id AS content_type_id,
o.approved AS approved,
o.record_label_id AS record_label_id
FROM
album as o,
(SELECT id FROM django_content_type where model='album') as ct
WHERE
NOT o.approved
UNION
SELECT
(SELECT nextval('unapproved_sequence')) as id,
o.id AS object_id,
ct.id AS content_type_id,
o.not_unapproved AS approved,
o.record_label_id AS record_label_id
FROM
dvd as o,
(SELECT id FROM django_content_type where model='dvd') as ct
WHERE
NOT o.not_unapproved
UNION
SELECT
(SELECT nextval('unapproved_sequence')) as id,
o.id AS object_id,
ct.id AS content_type_id,
o.approved AS approved,
NULL AS record_label_id
FROM
artist as o,
(SELECT id FROM django_content_type where model='artist') AS ct
WHERE
NOT o.approved
"""
Put that definition in app/models/unapproved.py, which is where the model will be going as well.
Those familiar with db views should probably skip on ahead to the next section.
Ok, what this gives you is a view called ‘unapproved_view’ which has the following fields:
- id
- object_id
- content_type_id
- approved
- record_label_id
Since artists do not have a record label foreign key, we specifically NULL it out. Note that we are only selecting rows that are not approved. If you’re trying this out with your own models, remember that for a UNION to work you need to have matching types.
The “nextval(‘unapproved_sequence’)” part is just to give each row in the unapproved_view a unique ID.
You may be tempted to optimize by replacing (SELECT id FROM django_content_type where model='artist') AS ct with a hard-coded content_type_id value, but that is much less safe. I’d rather have the overhead of those extra queries and not have to worry about content type ids changing on me.
Step 2: Model File
In app/models/unapproved.py:
class Unapproved(models.Model):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey()
approved = models.BooleanField(default=false)
record_label = models.ForeignKey(EvilRIAAMember, null=True)
class Meta:
db_table = "unapproved_view"
This isn’t the whole file of course, fill in the rest: imports, app_label, etc.
Things to note:- this model has “null=True” for the record_label definition to cater for Artists not having one.
- 0.96 and below should use models.GenericForeignKey instead of generic.GenericForeignKey
Anyway you now have a model file hooked to the view. You’ll have to create that view in your database first, of course (run the contents of the UNAPPROVED_VIEW variable from Step 1). Don’t forget about creating the sequence:
CREATE SEQUENCE unapproved_sequence START 1;
Step 3: Use the darn thing already
Now if you add the necessary “class Admin” boilerplate to the model, you’ll find that you can access this model in the Django admin. While you can’t view individual objects and they’re read-only, you do get a handy list view.
You can use this model in (Django) views and templates though. You can now do things like Unapproved.objects.filter(...). And with the GenericForeignKey you have easy access to the actual content object. Writing a view that displays all unapproved objects and lets user change the “approved” value to True is trivial once you have this set up, so I won’t get the details of that.
Hope the above explanation helped. This is a very useful technique since it allows you to practice DRY and handle common properties and behavior across your models in a single place.
1 Turns out the coding for Dvd model was outsourced to someone whose native language favors double negatives.
2 Yes, wise guys, in real life artists have record labels too, but since these can possibly change over time, for the purposes of this demo they don’t!
Inside Django's Template Inheritance
Users and abusers of Django’s nifty template inheritance may have found they are unable to do the following in a child template (one that uses the ‘extends’ templatetag):
{% block stuff %}
{% if cond %}
{% block foo %} override foo {% endblock %}
{% else %}
{% block bar %} override bar {% endblock %}
{% endif %}
{% endblock %}
What ends up happening is that both ‘foo’ and ‘bar’ blocks in the base template are overridden.
If you look at the Django source for the ‘block’ and ‘extends’ tags it is easy to see why this doesn’t work.
The render() function of the ExtendsNode class rips out every single block inside it and propagates it “up” to the base template by recursively replacing blocks in parent templates until the base template is reached. This takes place before the contents of block nodes are rendered.
The main consequences of this:
- Block nodes exist outside of flow control, as the snippet above shows
- Template rendering is sped up. All non-extends tags are rendered only once – when the base template is rendered.
Assume we really want to support the snippet I posted. This would result in a massively complex implementation, and sentences like
“Since template inheritance is somewhat similar to class inheritance, we need to implement dynamic dispatch of blocks via virtual function table. To populate the table…”
in the Django mailing list.
I think the current decision is the correct choice. It’s better to have a lightweight rendering engine and put the onus on the developer to get it right.
Here’s another fun thing to try before I go:
base template:{% block outer %}
I'm the outer block
{% block inner %}And I'm inside!{% endblock %}
{% endblock %}
child template:
{% block inner %}
I am so drunk that
{% block outer %}
I reversed the blocks
{% endblock %}
{% endblock %}
Try it!
Would you want the template rendering engine to include code to catch this and any of the myriad other gotchas compiler writers love so much? And have all that error-checking code chugging away in the background with every page you render? I think not.
Keeping up with Django and Rails 0
Working off the trunk of an open source project may seem scary because of all the uncertainties, but my experiences with Django and Rails tell me that this isn’t a matter of choice.
Well actually it is. If you want your project to be maintainable in the future, you have to keep up with the trunk of your framework. If not, then go ahead and pick a version to fix your project on. There you go, who said there aren’t any choices?
Quick aside: I don’t recall the latest Django trunk having an exciting “Gen-X PR-type” name1, so let’s call it “Django Axe2” (because Django is named after a guitarist, etcetc).
Of course I don’t mean that you update whenever Edge/Axe do. But you should do so frequently – once every week or two. While I haven’t been hit with this problem (because my projects actively keep up with Axe/Edge), I can tell from the changesets in both trunks that updating even 100 revisions can sometimes be very tedious.
This is because both frameworks are highly moving targets, with huge swaths of changes coming between versions. Axe recently had the entire Unicode branch merged, and Rails, well a look at the conversation thread here will give an idea of the magnitude of difference between stable and Edge.
What’s more, Django and Rails aren’t standalone projects – they are an entire ecosystem of common apps and plug-ins. And you can bet the most popular ones are going to move to follow Axe/Edge, so those components of your apps are going to be legacy as well.
Okay you say, but the reverse is true as well – what if you are relying on a plug-in that hasn’t upgraded? Well the responsible thing to do in that case is to get that upgrade done yourself, and contribute back. Chances of this happening are not that high unless you’re using something obscure, since most plug-ins for Django and Rails get a lot of love from users of their frameworks.
The consequences of not keeping up with a moving open source target are a lack of flexibility if you’re lucky, and extreme inefficiency if you aren’t:
1. Bug-fixes
While these projects have many instances of successful deployments, they are not without bugs. If one of these bugs suddenly surfaces in your project, you’re going to have to manually patch your version of the framework’s code. This may be fine the first, second, or third time. But keep going at it and the framework code you’re running on becomes a mess of custom backports.
And as time goes on, it becomes harder and harder to make such selective changes, since bugfix X relies on changeset Y, which depends on changeset Z, and so on. You will find yourself climbing up the dependency ladder each time you’re making a change. Soon you realize that the time you saved by not keeping up to date is far less than the time you’re expending to keep your Goldbergian series of patches intact.
2. New features
Say you have to roll-out a version of your Django site supporting other languages. If your Django code is close enough to the Unicode branch, you’ll have a much less painful time upgrading and fixing any issues that come about. Or your manager/client reads all the hype about OpenID, and you suddenly find yourself needing the OpenID plug-in that relies on Revision X. If you’re a thousand revisions behind, have fun upgrading. Or ignore the plug-in, roll your own, and pat yourself on the back. Neither way is as optimal as keeping up in the first place.
I understand that others may have been burned before by projects which had broken stuff in trunk that went unfixed for a long time. The conventional wisdom is to go with a stable release version.
But despite their disclaimers, Django and Rails generally take very good care of their trunks. I don’t think there is too much to worry about here. Plus you can always rollback a changeset or two if breakage occurs – I’m not saying you need every revision the moment it shows up on svn.
It may be a very tired analogy, but think of the time you spend every week or so updating your project to work on the latest framework code as stashing part of your salary away monthly. It gives you security, flexibility, and reduces your project risk.
1 Django doesn’t have a book with a skateboarder on the cover either.
2 Let’s see if lil ol’ me can start a meme.
Changing the Django Admin for Fun and Profit 0
One of the biggest annoyances of the Django admin app is the filter box on the right. Not its functionality—that is great. It’s the way it appears on top of the data, obscuring the rightmost columns. I generally hold the designers of Django in great regard, so I am kind of surprised they let something like this slip by.
Recently I was playing around with jQuery, and decided one of the things I could do with it is fix this problem. It doesn’t take too much code, with most of it boilerplate.
First, grab jQuery. Put it wherever you house the rest of your javascript files.
Next, decide the scope of apps/models you would like this change to apply to. See Customizing the Django admin in the Django book for more details. In this mini-tutorial I assume you would like to apply this change to one app. In that case, the code would go in:
<your template dir>/admin/<app_label>/change_list.html
By creating this template, you are telling Django to override all change lists in the Django admin for app <app_label> with that file.
And here’s the code:{% extends "admin/change_list.html" %}
{% block extrahead %}
<script type="text/javascript" src="YOUR_JS_DIR/jquery.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$("#changelist-filter h2").click(function() {
$("#changelist-filter ul").slideToggle("fast");
$("#changelist-filter h3").slideToggle("fast");
});
});
</script>
{% endblock %}
What this code does is add an event handler to the “Filter” heading at the very top of the filter list. Upon clicking this, it uses slideToggle() to hide or show the filter names (h3) and filter lists (ul). slideToggle() works by adjusting the height of the elements to toggle their visibility.
You don’t want to override the goodness that is already in the default change_list, so all you have to do is extend this template and add some javascript to the extrahead block.
And that’s it! Now you can hide the filter when it’s blocking columns you want to see fully.
Django: When save() is not safe 0
I’m looking forward to a stable version of newforms in the next Django release. The old forms and manipulators system, while decent, was the most painful part of Django to use—it seemed out of place amongst the rest of the framework.
Recently I ran into a situation which is not that out of the ordinary, but which the current manipulator-based admin make it very difficult for me to do.
Background: A “Book” has a main_category field and a categories field, with the former providing the category slug used in the url. The main_category field is a foreign key to the Category model, and categories has a many-to-many relationship with Category.
Objective: In order to make searching one line of code, copy the category chosen in main_category to categories upon save. That simplifies queries for Books based on category to a single books = desired_category.book_set.all() call. (Without the save I would have to OR this with Books.objects.filter(main_category=desired_category)).
So, sounds pretty straightforward to those with any experience in Django—modify the save() function in Books to add whatever is in main_category to categories as follows:
self.categories.add(self.main_category)
super(Book, self).save()
This works perfectly in ipython.
However, when I tested this through Django’s admin interface, it did not work upon save, ie main_category is not copied over.
This is because saving in admin goes through the AutomaticManipulator. Ample pdb usage found the code in question in db/models/manipulators.py. I’ll paste the relevant extracts here, comments enclosed in ### are my annotations.
def save(self, new_data):
### [ snipped ] ###
# First, save the basic object itself.
new_object = self.model(**params)
new_object.save()
### post_save hook kicks in at the end of save() above ###
### [ snipped ] ###
# Save many-to-many objects. Example: Set sites for a poll.
for f in self.opts.many_to_many:
if self.follow.get(f.name, None):
if not f.rel.edit_inline:
if f.rel.raw_id_admin:
new_vals = new_data.get(f.name, ())
else:
new_vals = new_data.getlist(f.name)
# First, clear the existing values.
rel_manager = getattr(new_object, f.name)
### Sacre bleu! what is this? ###
rel_manager.clear()
# Then, set the new values.
for n in new_vals:
rel_manager.add(f.rel.to._default_manager.get(pk=n))
Ok, here’s what happens.
- My overridden save method gets called in
new_object.save() - rel_manager deletes everything that used to be in categories with rel_manager.clear() (found in db/models/fields/related.py
The post_save hook does not help here, since it kicks in at the end of new_object.save().
Essentially, the code for saving many-to-many objects works by deleting what is already there and adding whatever was included in the form. All my overriding of the save() function in Book managed to do was provide one more category to be deleted.
Now I’m not saying this is the wrong thing to do—from the admin’s point of view, the correct thing to do is ignore whatever was in categories before, and only look at what has been selected in the form field. It is the right thing to do within the current application structure, imo.
However, when that application structure limits me from doing something that should be fairly straightforward (adding to a many2many field programmatically in the admin), then maybe there is some room for improvement.
I haven’t had the time to have a comprehensive look at newforms yet, but my brief skimming of related posts on newforms leads me to think that it will eventually be a change near the same magnitude of magic-removal. Just as I am glad I only came across Django post magic-removal, I’m sure future users of Django will be glad they didn’t have to deal with oldforms—such is the improvement newforms will provide. Those interested in getting a headstart can find an excellent intro to newforms here.
Managing Django's Managers 0
Managers in Django are perhaps a misnomer, because unlike the stereotypical PHB they are flexible and provide a very efficient way of doing things.
But while Managers are very powerful, they can cause problems if you aren’t familiar with the way they work behind the scenes. Hopefully this post will help you avoid a problem I ran into.
Some time ago our client wanted to change the way a model worked—they wanted certain objects to be invisible on the front end but still remain present on the back-end admin app.
Using a custom manager, you can accomplish that without changing the hundreds of ORM calls you have in your code. In this situation, say we want to filter out all Albums that aren’t in stock—they should never be shown in the front end. Now there are probably lots of queries like this all over your code1:Album.objects.filter(category__title="Metal")
Now, going through all such calls and adding a “in_stock=True” to every filter would be a very painful task. It can be automated with sed, but it causes a lot of unnecessary repetition.
So you can just define a custom Manager:
class AlbumInStockManager(models.Manager):
def get_query_set(self):
return super(AlbumInStockManager, self).get_query_set().filter(
in_stock=True)
And use that manager in your model:
class Album(models.Model):
objects = AlbumInStockManager()
And voila! All queries using Album.objects will now only return albums where in_stock is true.
However, this has a side effect2:
If you use custom Manager objects, take note that the first Manager Django encounters (in order by which they’re defined in the model) has a special status. Django interprets the first Manager defined in a class as the “default” Manager. Certain operations – such as Django’s admin site – use the default Manager to obtain lists of objects, so it’s generally a good idea for the first Manager to be relatively unfiltered.
Actually, make that “completely unfiltered”. The reason is explained in Ticket 1855, “Using a custom default manager can lead to un-editable objects in admin” :
This is because the admin change_stage view uses the automatic ChangeManipulator for the model, which in turn uses the model’s default manager to fetch the object to change. So if the model’s default manager happens to filter in a way which excludes that object from the returned QuerySet, the ChangeManipulator will raise ObjectDoesNotExist, which in turn causes the admin to return a 404.
Our declaration of a custom manager has resulted in overriding the default manager and breaks part of admin functionality due to this bug.
So the simple solution here is to keep “objects” as the default manager, and use a different manager for the front-end Album queries:
objects = model.Manager() # default manager
in_stock = AlbumInStockManager() # custom manager
However, this means that all previous Album.objects calls in the front-end would have to be change to Album.in_stock calls.
So, how do you have your cake and eat it too?
The correct solution for our requirements (keep Admin untouched and use the name “objects” for our custom manager):
# define all_objects first
# so it becomes the default used by admin
all_objects = models.Manager()
objects = AlbumInStockManager()
But we’re not out of the woods yet. Calls to Django helper functions like get_object_or_404 use the default manager too.
So, in the front-end where you only want in-stock Albums to be displayed, you have to change your calls:
# WRONG - uses models.manager()
one_hit_wonder = get_object_or_404(Album,
title="Who Let The Dogs Out")
# RIGHT - uses objects = AlbumInStockManager
one_hit_wonder = get_object_or_404(Album.objects,
title="Who Let The Dogs Out")
(This works as of revision 4275 which lets get_object_or_404 take Manager objects)
To recap
- The built-in admin app and various helper functions use the default manager
- The admin app will not allow editing of objects filtered out by the default manager
- This default manager will be:
- the generic one if no managers are defined
- the first defined manager if one or more managers are defined
Now I do not really like the idea of a convention, documented or no, where the first defined manager automagically becomes the default manager. Hopefully in the future there is a much clearer way of specifying the default manager regardless of where it appears in the class definition.
For more information, Ticket 1855 contains a highly informative discussion.
1 “objects” is the default Django manager that gets included with every class. It yields querysets that perform no filtering, ie return all rows
2 from model documentation
Handling Legacy Data 2
So, I’ve spent the better part of the past couple days trying to import legacy data into our brand new spiffy models.
And trust me, shoehorning 5-6 years’ worth of inconsistent data into new models is not fun. I don’t begrudge those in the data mining industry the bundles of cash they must make—this is one tedious PITA task.
The worst thing about a one-off migration is that you don’t learn that much from it. True, I made mistakes and learned from them principles that I can reuse if I ever suffer through a similar task, but for the most part it is very domain-specific knowledge that I will not get to practice again.
One part of this arduous process was converting from HTML to plaintext. Straightforward enough. The previous article bodies contained a lot of entities like our good friend , so right off the bat your standard naive regular expression stripping is not feasible1. BeautifulSoup would have worked, but it kept segfaulting on me. So my next attempt was to roll my own:
def html2plaintext(html):
out = StringIO()
formatter = AbstractFormatter(DumbWriter(out, 200))
parser = htmllib.HTMLParser(formatter)
parser.feed(html)
parser.close()
return out.getvalue()
Now this is very cleaned up—prior to this I was using a file object instead of StringIO2. But it works, with a little mucking around with decode() and encode(). This result, however, was not completely optimal, since what was originally formatted decently with HTML turned into an unattractive chunk of plaintext3.
I’ve saved the real gem of this entire exercise till the end—a little library with the highly original name html2text. It has the benefit of not just converting to plain text, but to valid Markdown as well. Perfect for our purposes, since we use Markdown with django.contrib.markup4. Oh, and did I mention it only took one line? (My code, not the html2text code – this is Python, not Perl
)
This time round I had results I was completely satisfied with.
This episode only served to remind me how rich Python’s libraries are. For my task I had the option of building off Python’s standard libraries, or deciding between external libraries such as the more general BeautifulSoup and the lesser-known library that satisfied all my task requirements – html2text. Python users are seriously spoilt for choice, and that’s what makes Python a joy to work with.
1 Indeed it is almost never feasible, due to the irregularity of most HTML you find on the web. Cue gratuitous usage of jwz’s famous quote: “Some people, when confronted with a problem, think ‘I know, I’ll use regular expressions.’ Now they have two problems. “
2 When I write one-off code, it sometimes gets ugly enough to make the Daily WTF because I become a lot less concerned about maintainability in the name of Getting Things Done (I suppose this phrase is trademarked by Spolsky?)
3 The <p>s and <br>s were turned into newlines of course, but the rest of the formatting was lost.
4 Unfortunately this nomenclature is confusing the first couple times you run into it. You have to {% load markup %} to use the {{ content|markdown }} filter. But I understand where it comes from—the markup app covers not only Markdown but Textile and ReST (reST? rEsT? 4357?)
Django Book 1
First off, kudos to those involved for publishing this book online with a GNU Free Document License1. Nice to see that reaching a wider audience takes priority over making a quick buck or two.
The first two chapters of the book are currently located at http://www.djangobook.com/. Check out its extremely slick comments system while you’re there.
A per-block comment system is an idea I toyed with for awhile, but I never figured out a reasonable interface for it. The Django Book does it very elegantly—I was more wow-ed by the UI design than the tech side of it to be honest. Linux + Firefox 1.5 users will get a warning dialog box – just hit “continue” instead of “stop script”. Firefox 2 handles the page just fine.
My only gripe with the book is that I wish it was written backwards! I’ve worked with Django for a while now, so I’d have a lot more to gain from the more advanced chapters. Will definitely keep tabs on the chapters as they come in though for a fresh perspective on things.
Those of you wanting to get your feet wet, don’t wait for the book. The official documentation on the project site is more than adequate.
1 Which reminds me – I really need to put a Creative Commons notice in my template.
Typo Powered 4
I use Python with the Django framework at work, and I enjoy working with them. Django really simplifies development, and has some excellent sites built using it: have a look at Naples News Online and Lawrence.com
So when it came to putting this blog up, the first thought that came to my mind was “Ok, I’ll spend a couple hours putting up something basic with Django, and I can then add to it as I go along.”
Yep, trust my first instinct to be the most lazy time-saving one.
Then I realized that this option was just too easy. Having built an entire CMS with Django recently, I would learn nothing new by doing a simple blog with it.
So I decided this was an excellent chance to learn Ruby and RoR. I’d be killing two birds with one stone: diversifying my knowledge portfolio within the area of web development, and sating my intellectual curiosity as to why Rails has exploded over the past year or so.
Seemed simple enough. Checked out the Pickaxe book from my company library, and got the seminal Rails book too. Went through the tutorial, and subsequently put together a bare-bones blog app (sans a decent template and styling) in around half an hour.
Yes, that’s not a typo. Neither am I exaggerating. Rails is simply that fast. Bear in mind we’re talking bare minimum here—all my app was at that point was a simple wrapper around a BlogEntry model with Title, Body, and CreatedAt fields. But I could perform basic CRUD on it and have the results immediately reflected live. A minimalist blog I suppose. Django would be slightly slower, because it eschews the script/generate “magic” of Rails1.
Alright, there’s enough praise and hyperbole about Rails out there. No point adding my voice to the choir.
Anyway, from that base I planned to slowly bake in basic features like comments, tags, etc.
That’s when Captain Obvious paid me a visit saying, “You do realize that what you’re doing is a classic example of NIH syndrome.” I then realized that I could learn about Rails faster by using one of the blog platforms out there built on Rails and contributing back to it, instead of reinventing the wheel as I originally planned.
In the end I went with Typo. Not because I find it hilarious to have a blog that is “Typo Powered2”. And I did this despite the fact that their Trac page was down (and is still down) due to spam issues. Despite the fact its developers go by the rather dubious name of Leetsoft3. Despite the fact that Steve Yegge crucified Typo for its lack of documentation and difficulty of installation. I did find it easy to set up though, thanks to an excellent tutorial I found.
I initially chose Typo because it seemed to be the blog of choice for people playing around with Rails. I’ve stayed with it because its been a blast to use so far, despite the occasional bug I run into (which I could help with or at least look up if their Trac was up!) Lacks the polish of Wordpress et al, but makes up for it with some very nice glitz.
Thus for now, this blog is Typo Powered. It took me a long time to settle on an option—I went from self-built Django to self-built Rails to Typo, but I’m glad I took my time making a decision.
1 I’ll cover ideological differences between Django and Rails in the future when I’m more familiar with the latter.
2 I do find it hilarious, but that’s definitely not the basis of my decision-making.
3 OMGWTFBBQpewpew j00 lam3rz, everyone knowz its spelled as “l33t”!!!111one

