steve losh
 

Django Advice

Flattr

Posted on June 30, 2011.

For the past year and a half or so I’ve been working full-time at Dumbwaiter Design doing Django development. I’ve picked up a bunch of useful tricks along the way that help me work, and I figured I’d share them.

I’m sure there are better ways to do some of the things that I mention. If you know of any feel free to hit me up on Twitter and let me know.

Also: this entry was written over several months, so if there are inconsistencies let me know and I’ll try to fix them.

  • Vagrant
    • Why Vagrant?
    • Using Fabric to Stay Fast and Automate Everything
  • Wrangling Databases with South
    • Useful Fabric Tasks
  • Watching for Changes
    • Using the Werkzeug Debugger with Gunicorn
    • Pulling Uploads
    • Preventing Accidents
  • Working with Third-Party Apps
    • Installing Apps from Repositories
    • Mirroring Repositories
    • Using BCVI to Edit Files
  • Improving the Admin Interface
    • Enter Grappelli
    • An Ugly Hack to Show Usable Foreign Key Fields
  • Using Django-Annoying
    • The render_to Decorator
    • The ajax_request Decorator
  • Templating Tricks
    • Null Checks and Fallbacks
    • Manipulating Query Strings
    • Satisfying Your Designer with Typogrify
  • The Flat Page Trainwreck
  • Editing with Vim
    • Vim for Django
    • Filetype Mappings
    • Python Sanity Checking
    • Javascript Sanity Checking and Folding
    • Django Autocommands
  • Conclusion

Vagrant

I used to develop Django sites by running them on my OS X laptop locally and deploying to a Linode VPS. I had a whole section of this post written up about tricks and tips for working with that setup.

Then I found Vagrant.

I just deleted the entire section of this post I wrote.

Vagrant gives you a better way of working. You need to use it.

Why Vagrant?

If you haven’t used it before, Vagrant is basically a tool for managing VirtualBox VMs. It makes it easy to start, pause, and resume VMs. Instead of installing Django in a virtualenv and developing against that, you run a VM which runs your site and develop against that.

This may not sound like much, but it’s kind of a big deal. The critical difference is that you can now develop against the same setup that you’ll be using in production.

This cuts out a huge amount of pain that stems from OS differences. Here are a few examples off the top of my head:

  • URLField and MacPorts Python 2.5 on OS X. There’s a bug where using verify_exists will crash your site every time you save a model, unless you set a particular environment variable with no debug information. Yeah, I spent a couple of hours tracking that one down at work. Awesome.
  • Installing PIL on OS X is no picnic. homebrew makes things better, if you use it, so this one isn’t a huge deal.
  • Every time you update Python in-place on your local machines, ALL of your virtualenvs break because the Python binaries inside are linked against global Python library files. Have fun recreating them. I hope you froze your requirements.txt files before you updated.

Using Vagrant and VMs means you can just worry about ONE operating system and its quirks. It saves you a ton of time.

Aside from that, there’s another benefit to using Vagrant: it strongly encourages you to learn and use an automated provisioning system. Support for Puppet and Chef is built in. I chose Puppet, but if you prefer Chef that’s cool too.

You can also use other tools like Fabric or some simple scripts, but I’d strongly recommend giving Puppet or Chef a fair shot. It’s a lot to learn, but they’re both widely tested and very powerful.

Because you’re developing against a VM and deploying to a VM, you can reuse 90% of the provisioning code across the two.

When I make a new site, I do the following to initialize a new Vagrant VM:

  1. vagrant up (which runs Puppet to initialize the VM)
  2. fab dev bootstrap

When I’m ready to go live, I do the following:

  1. Buy a Linode VPS.
  2. Run Puppet to initialize the VPS.
  3. Enter the Linode info in my fabfile.
  4. fab prod bootstrap

No more screwing around with different paths, different versions of Nginx, different versions of Python. When I’m developing something I can be pretty confident it will “just work” in production without any major surprises.

Using Fabric to Stay Fast and Automate Everything

One of the problems with this setup is that I can’t just run python manage.py whatever any more because I need it to run on the VM.

To get around this I’ve created many simple Fabric tasks to automate the common things I need to do. Fabric is an awesome little Python utility for scripting tasks (like deployments). We use it constantly at Dumbwaiter. Here are a few examples from our fabfiles.

This first set is for running abitrary commands easily.

cmd and vcmd will cd into the site directory on the VM and run a command of my choosing. vcmd will prefix the command with the path to the virtualenv’s bin directory, so I can do something like fab dev vcmd, pip install markdown.

The sdo commands do the same thing, but sudo‘ed.

def cmd(cmd=""):
    '''Run a command in the site directory.  Usable from other commands or the CLI.'''
    require('site_path')

    if not cmd:
        sys.stdout.write(_cyan("Command to run: "))
        cmd = raw_input().strip()

    if cmd:
        with cd(env.site_path):
            run(cmd)

def sdo(cmd=""):
    '''Sudo a command in the site directory.  Usable from other commands or the CLI.'''
    require('site_path')

    if not cmd:
        sys.stdout.write(_cyan("Command to run: sudo "))
        cmd = raw_input().strip()

    if cmd:
        with cd(env.site_path):
            sudo(cmd)

def vcmd(cmd=""):
    '''Run a virtualenv-based command in the site directory.  Usable from other commands or the CLI.'''
    require('site_path')
    require('venv_path')

    if not cmd:
        sys.stdout.write(_cyan("Command to run: %s/bin/" % env.venv_path.rstrip('/')))
        cmd = raw_input().strip()

    if cmd:
        with cd(env.site_path):
            run(env.venv_path.rstrip('/') + '/bin/' + cmd)

def vsdo(cmd=""):
    '''Sudo a virtualenv-based command in the site directory.  Usable from other commands or the CLI.'''
    require('site_path')
    require('venv_path')

    if not cmd:
        sys.stdout.write(_cyan("Command to run: sudo %s/bin/" % env.venv_path.rstrip('/')))
        cmd = raw_input().strip()

    if cmd:
        with cd(env.site_path):
            sudo(env.venv_path.rstrip('/') + '/bin/' + cmd)

This next set is just some common commands that I need to run often.

def syncdb():
    '''Run syncdb.'''
    require('site_path')
    require('venv_path')

    with cd(env.site_path):
        run(_python('manage.py syncdb --noinput'))

def collectstatic():
    '''Collect static media.'''
    require('site_path')
    require('venv_path')

    with cd(env.site_path):
        sudo(_python('manage.py collectstatic --noinput'))

def rebuild_index():
    '''Rebuild the search index.'''
    require('site_path')
    require('venv_path')
    require('process_owner')

    with cd(env.site_path):
        sudo(_python('manage.py rebuild_index'))
        sudo('chown -R %s .xapian' % env.process_owner)

def update_index():
    '''Update the search index.'''
    require('site_path')
    require('venv_path')
    require('process_owner')

    with cd(env.site_path):
        sudo(_python('manage.py update_index'))
        sudo('chown -R %s .xapian' % env.process_owner)

We also use Fabric to automate some of the more complex things we need to do.

This task curl‘s the site’s home page to make sure we haven’t completely borked things. We use it in lots of other tasks as a sanity check.

def check():
    '''Check that the home page of the site returns an HTTP 200.'''
    require('site_url')

    print('Checking site status...')

    if not '200 OK' in local('curl --silent -I "%s"' % env.site_url, capture=True):
        _sad()
    else:
        _happy()

The _happy and _sad functions just print out some simple messages to get our attention:

from fabric.colors import red, green

def _happy():
    print(green('\nLooks good from here!\n'))

def _sad():
    print(red(r'''
          ___           ___
         /  /\         /__/\
        /  /::\        \  \:\
       /  /:/\:\        \__\:\
      /  /:/  \:\   ___ /  /::\
     /__/:/ \__\:\ /__/\  /:/\:\
     \  \:\ /  /:/ \  \:\/:/__\/
      \  \:\  /:/   \  \::/
       \  \:\/:/     \  \:\
        \  \::/       \  \:\
         \__\/         \__\/
          ___           ___           ___           ___
         /__/\         /  /\         /  /\         /  /\     ___
         \  \:\       /  /::\       /  /:/_       /  /:/_   /__/\
          \  \:\     /  /:/\:\     /  /:/ /\     /  /:/ /\  \  \:\
      _____\__\:\   /  /:/  \:\   /  /:/ /:/_   /  /:/ /::\  \  \:\
     /__/::::::::\ /__/:/ \__\:\ /__/:/ /:/ /\ /__/:/ /:/\:\  \  \:\
     \  \:\~~\~~\/ \  \:\ /  /:/ \  \:\/:/ /:/ \  \:\/:/~/:/   \  \:\
      \  \:\  ~~~   \  \:\  /:/   \  \::/ /:/   \  \::/ /:/     \__\/
       \  \:\        \  \:\/:/     \  \:\/:/     \__\/ /:/          __
        \  \:\        \  \::/       \  \::/        /__/:/          /__/\
         \__\/         \__\/         \__\/         \__\/           \__\/

         Something seems to have gone wrong!
         You should probably take a look at that.
    '''))

This one is for when python manage.py reset APP is broken because you’ve changed some db_column names and Django chokes because of some constraits and you just want to reset the fucking app.

It’s the “NUKE IT FROM ORBIT!!” option.

def KILL_IT_WITH_FIRE(app):
    require('site_path')
    require('venv_path')

    with cd(env.site_path):
        # Generate and download the reset SQL.
        sudo(_python('manage.py sqlreset %s > reset.orig.sql' % app))
        get('reset.orig.sql')

        with open('reset.sql', 'w') as f:
            with open('reset.orig.sql') as orig:
                # Step through the first chunk of the file (the "drop" part).
                line = orig.readline()
                while not line.startswith('CREATE'):
                    if 'CONSTRAINT' in line:
                        # Don't write out CONSTRAINT lines.
                        # They're a problem when you change db_colum names.
                        pass
                    elif 'DROP TABLE' in line:
                        # Cascade drops.
                        # Hence with "with fire" part of this task's name.
                        line = line[:-2] + ' CASCADE;\n'
                        f.write(line)
                    else:
                        # Write other lines through untoched.
                        f.write(line)
                    line = orig.readline()

                # Write out the rest of the file untouched.
                f.write(line)
                f.write(orig.read())

    # Upload the processed SQL file.
    put('reset.sql', os.path.join(env.site_path, 'reset.ready.sql'), use_sudo=True)

    with cd(env.site_path):
        # Use the SQL to reset the app, and fake a migration.
        run(_python('manage.py dbshell < reset.ready.sql'))
        sudo(_python('manage.py migrate --fake --delete-ghost-migrations ' + app))

This task uses Mercurial’s local tags to add a production or staging tag in your local repository, so you can easy see where the production/staging servers are at compared to your local repo.

def retag():
    '''Check which revision the site is at and update the local tag.

    Useful if someone else has deployed (which makes your production/staging local
    tag incorrect.
    '''
    require('site_path', provided_by=['prod', 'stag'])
    require('env_name', provided_by=['prod', 'stag'])

    with cd(env.site_path):
        current = run('hg id --rev . --quiet').strip(' \n+')

    local('hg tag --local --force %s --rev %s' % (env.env_name, current))

This task tails the Gunicorn logs on the server so you can quickly find out what’s happening when things blow up.

def tailgun(follow=''):
    """Tail the Gunicorn log file."""
    require('site_path')

    with cd(env.site_path):
        if follow:
            run('tail -f .gunicorn.log')
        else:
            run('tail .gunicorn.log')

We’ve got a lot of other tasks but they’re pretty specific to our setup.

Wrangling Databases with South

If you’re not using South, you need to start. Now.

No, really, I’ll wait. Take 30 minutes, try the tutorial, wrap your head around it and come back. It’s far more important than this blog post.

Useful Fabric Tasks

South is awesome but its commands are very long-winded. Here’s the set of fabric tasks I use to save quite a bit of typing:

def migrate(args=''):
    '''Run any needed migrations.'''
    require('site_path')
    require('venv_path')

    with cd(env.site_path):
        sudo(_python('manage.py migrate ' + args))

def migrate_fake(args=''):
    '''Run any needed migrations with --fake.'''
    require('site_path')
    require('venv_path')

    with cd(env.site_path):
        sudo(_python('manage.py migrate --fake ' + args))

def migrate_reset(args=''):
    '''Run any needed migrations with --fake.  No, seriously.'''
    require('site_path')
    require('venv_path')

    with cd(env.site_path):
        sudo(_python('manage.py migrate --fake --delete-ghost-migrations ' + args))

Remember that running a migration without specifying an app will migrate everything, so a simple fab dev migrate will do the trick.

Watching for Changes

When developing locally you’ll want to make a change to your code and have the server reload that code automatically. The Django development server does this, and we can hack it into our Vagrant/Gunicorn setup too.

First, add a monitor.py file at the root of your project (I believe I found this code here, but I may be wrong):

import os
import sys
import time
import signal
import threading
import atexit
import Queue

_interval = 1.0
_times = {}
_files = []

_running = False
_queue = Queue.Queue()
_lock = threading.Lock()

def _restart(path):
    _queue.put(True)
    prefix = 'monitor (pid=%d):' % os.getpid()
    print >> sys.stderr, '%s Change detected to \'%s\'.' % (prefix, path)
    print >> sys.stderr, '%s Triggering process restart.' % prefix
    os.kill(os.getpid(), signal.SIGINT)

def _modified(path):
    try:
        # If path doesn't denote a file and were previously
        # tracking it, then it has been removed or the file type
        # has changed so force a restart. If not previously
        # tracking the file then we can ignore it as probably
        # pseudo reference such as when file extracted from a
        # collection of modules contained in a zip file.

        if not os.path.isfile(path):
            return path in _times

        # Check for when file last modified.

        mtime = os.stat(path).st_mtime
        if path not in _times:
            _times[path] = mtime

        # Force restart when modification time has changed, even
        # if time now older, as that could indicate older file
        # has been restored.

        if mtime != _times[path]:
            return True
    except:
        # If any exception occured, likely that file has been
        # been removed just before stat(), so force a restart.

        return True

    return False

def _monitor():
    while 1:
        # Check modification times on all files in sys.modules.

        for module in sys



gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.