982 lines
No EOL
75 KiB
HTML
982 lines
No EOL
75 KiB
HTML
<!DOCTYPE html>
|
|
<html lang='en'><head><meta charset='utf-8' /><meta name='pinterest' content='nopin' /><link href='../../../../static/css/style.css' rel='stylesheet' type='text/css' /><link href='../../../../static/css/print.css' rel='stylesheet' type='text/css' media='print' /><title>Django Advice / Steve Losh</title></head><body><header><a id='logo' href='https://stevelosh.com/'>Steve Losh</a><nav><a href='../../../index.html'>Blog</a> - <a href='https://stevelosh.com/projects/'>Projects</a> - <a href='https://stevelosh.com/photography/'>Photography</a> - <a href='https://stevelosh.com/links/'>Links</a> - <a href='https://stevelosh.com/rss.xml'>Feed</a></nav></header><hr class='main-separator' /><main id='page-blog-entry'><article><h1><a href='index.html'>Django Advice</a></h1><p class='date'>Posted on June 30th, 2011.</p><p>For the past year and a half or so I've been working full-time at <a href="http://dwaiter.com/">Dumbwaiter
|
|
Design</a> doing <a href="http://djangoproject.com/">Django</a> development. I've picked up a bunch of useful tricks along
|
|
the way that help me work, and I figured I'd share them.</p>
|
|
|
|
<p>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 <a href="http://twitter.com/stevelosh">Twitter</a> and let me know.</p>
|
|
|
|
<p>Also: this entry was written over several months, so if there are inconsistencies let
|
|
me know and I'll try to fix them.</p>
|
|
|
|
<ol class="table-of-contents"><li><a href="index.html#s1-vagrant">Vagrant</a><ol><li><a href="index.html#s2-why-vagrant">Why Vagrant?</a></li><li><a href="index.html#s3-using-fabric-to-stay-fast-and-automate-everything">Using Fabric to Stay Fast and Automate Everything</a></li></ol></li><li><a href="index.html#s4-wrangling-databases-with-south">Wrangling Databases with South</a><ol><li><a href="index.html#s5-useful-fabric-tasks">Useful Fabric Tasks</a></li></ol></li><li><a href="index.html#s6-watching-for-changes">Watching for Changes</a><ol><li><a href="index.html#s7-using-the-werkzeug-debugger-with-gunicorn">Using the Werkzeug Debugger with Gunicorn</a></li><li><a href="index.html#s8-pulling-uploads">Pulling Uploads</a></li><li><a href="index.html#s9-preventing-accidents">Preventing Accidents</a></li></ol></li><li><a href="index.html#s10-working-with-third-party-apps">Working with Third-Party Apps</a><ol><li><a href="index.html#s11-installing-apps-from-repositories">Installing Apps from Repositories</a></li><li><a href="index.html#s12-mirroring-repositories">Mirroring Repositories</a></li><li><a href="index.html#s13-using-bcvi-to-edit-files">Using BCVI to Edit Files</a></li></ol></li><li><a href="index.html#s14-improving-the-admin-interface">Improving the Admin Interface</a><ol><li><a href="index.html#s15-enter-grappelli">Enter Grappelli</a></li><li><a href="index.html#s16-an-ugly-hack-to-show-usable-foreign-key-fields">An Ugly Hack to Show Usable Foreign Key Fields</a></li></ol></li><li><a href="index.html#s17-using-django-annoying">Using Django-Annoying</a><ol><li><a href="index.html#s18-the-render-to-decorator">The render_to Decorator</a></li><li><a href="index.html#s19-the-ajax-request-decorator">The ajax_request Decorator</a></li></ol></li><li><a href="index.html#s20-templating-tricks">Templating Tricks</a><ol><li><a href="index.html#s21-null-checks-and-fallbacks">Null Checks and Fallbacks</a></li><li><a href="index.html#s22-manipulating-query-strings">Manipulating Query Strings</a></li><li><a href="index.html#s23-satisfying-your-designer-with-typogrify">Satisfying Your Designer with Typogrify</a></li></ol></li><li><a href="index.html#s24-the-flat-page-trainwreck">The Flat Page Trainwreck</a></li><li><a href="index.html#s25-editing-with-vim">Editing with Vim</a><ol><li><a href="index.html#s26-vim-for-django">Vim for Django</a></li><li><a href="index.html#s27-filetype-mappings">Filetype Mappings</a></li><li><a href="index.html#s28-python-sanity-checking">Python Sanity Checking</a></li><li><a href="index.html#s29-javascript-sanity-checking-and-folding">Javascript Sanity Checking and Folding</a></li><li><a href="index.html#s30-django-autocommands">Django Autocommands</a></li></ol></li><li><a href="index.html#s31-conclusion">Conclusion</a></li></ol>
|
|
|
|
<h2 id="s1-vagrant"><a href="index.html#s1-vagrant">Vagrant</a></h2>
|
|
|
|
<p>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.</p>
|
|
|
|
<p>Then I found <a href="http://vagrantup.com/">Vagrant</a>.</p>
|
|
|
|
<p>I just deleted the entire section of this post I wrote.</p>
|
|
|
|
<p>Vagrant gives you a better way of working. You need to use it.</p>
|
|
|
|
<h3 id="s2-why-vagrant"><a href="index.html#s2-why-vagrant">Why Vagrant?</a></h3>
|
|
|
|
<p>If you haven't used it before, Vagrant is basically a tool for managing
|
|
<a href="http://www.virtualbox.org/">VirtualBox</a> 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.</p>
|
|
|
|
<p>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.</p>
|
|
|
|
<p>This cuts out a huge amount of pain that stems from OS differences. Here are a few
|
|
examples off the top of my head:</p>
|
|
|
|
<ul>
|
|
<li>URLField and MacPorts Python 2.5 on OS X. There's a <a href="https://trac.macports.org/ticket/24421">bug</a> 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.</li>
|
|
<li>Installing PIL on OS X is no picnic. <a href="http://mxcl.github.com/homebrew/">homebrew</a> makes things better, if you use it,
|
|
so this one isn't a huge deal.</li>
|
|
<li>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
|
|
<code>requirements.txt</code> files before you updated.</li>
|
|
</ul>
|
|
|
|
<p>Using Vagrant and VMs means you can just worry about ONE operating system and its
|
|
quirks. It saves you a ton of time.</p>
|
|
|
|
<p>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.</p>
|
|
|
|
<p>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.</p>
|
|
|
|
<p>Because you're developing against a VM and deploying to a VM, you can reuse 90% of
|
|
the provisioning code across the two.</p>
|
|
|
|
<p>When I make a new site, I do the following to initialize a new Vagrant VM:</p>
|
|
|
|
<ol>
|
|
<li><code>vagrant up</code> (which runs Puppet to initialize the VM)</li>
|
|
<li><code>fab dev bootstrap</code></li>
|
|
</ol>
|
|
|
|
<p>When I'm ready to go live, I do the following:</p>
|
|
|
|
<ol>
|
|
<li>Buy a Linode VPS.</li>
|
|
<li>Run Puppet to initialize the VPS.</li>
|
|
<li>Enter the Linode info in my fabfile.</li>
|
|
<li><code>fab prod bootstrap</code></li>
|
|
</ol>
|
|
|
|
<p>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.</p>
|
|
|
|
<h3 id="s3-using-fabric-to-stay-fast-and-automate-everything"><a href="index.html#s3-using-fabric-to-stay-fast-and-automate-everything">Using Fabric to Stay Fast and Automate Everything</a></h3>
|
|
|
|
<p>One of the problems with this setup is that I can't just run <code>python manage.py
|
|
whatever</code> any more because I need it to run on the VM.</p>
|
|
|
|
<p>To get around this I've created many simple <a href="http://fabfile.org/">Fabric</a> 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.</p>
|
|
|
|
<p>This first set is for running abitrary commands easily.</p>
|
|
|
|
<p><code>cmd</code> and <code>vcmd</code> will <code>cd</code> into the site directory on the VM and run a command of my
|
|
choosing. <code>vcmd</code> will prefix the command with the path to the virtualenv's <code>bin</code>
|
|
directory, so I can do something like <code>fab dev vcmd</code>, <code>pip install markdown</code>.</p>
|
|
|
|
<p>The <code>sdo</code> commands do the same thing, but <code>sudo</code>'ed.</p>
|
|
|
|
<pre><code><span class="code"><span class="special">def</span><span class="keyword"> cmd</span><span class="paren1">(<span class="code">cmd=<span class="string">""</span></span>)</span>:
|
|
<span class="string">'''Run a command in the site directory. Usable from other commands or the CLI.'''</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'site_path'</span></span>)</span>
|
|
|
|
|
|
<span class="symbol">if</span> <span class="symbol">not</span> cmd:
|
|
sys.stdout.write<span class="paren1">(<span class="code">_cyan<span class="paren2">(<span class="code"><span class="string">"Command to run: "</span></span>)</span></span>)</span>
|
|
cmd = raw_input<span class="paren1">(<span class="code"></span>)</span>.strip<span class="paren1">(<span class="code"></span>)</span>
|
|
|
|
<span class="symbol">if</span> cmd:
|
|
with cd<span class="paren1">(<span class="code">env.site_path</span>)</span>:
|
|
run<span class="paren1">(<span class="code">cmd</span>)</span>
|
|
|
|
<span class="special">def</span><span class="keyword"> sdo</span><span class="paren1">(<span class="code">cmd=<span class="string">""</span></span>)</span>:
|
|
<span class="string">'''Sudo a command in the site directory. Usable from other commands or the CLI.'''</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'site_path'</span></span>)</span>
|
|
|
|
<span class="symbol">if</span> <span class="symbol">not</span> cmd:
|
|
sys.stdout.write<span class="paren1">(<span class="code">_cyan<span class="paren2">(<span class="code"><span class="string">"Command to run: sudo "</span></span>)</span></span>)</span>
|
|
cmd = raw_input<span class="paren1">(<span class="code"></span>)</span>.strip<span class="paren1">(<span class="code"></span>)</span>
|
|
|
|
<span class="symbol">if</span> cmd:
|
|
with cd<span class="paren1">(<span class="code">env.site_path</span>)</span>:
|
|
sudo<span class="paren1">(<span class="code">cmd</span>)</span>
|
|
|
|
<span class="special">def</span><span class="keyword"> vcmd</span><span class="paren1">(<span class="code">cmd=<span class="string">""</span></span>)</span>:
|
|
<span class="string">'''Run a virtualenv-based command in the site directory. Usable from other commands or the CLI.'''</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'site_path'</span></span>)</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'venv_path'</span></span>)</span>
|
|
|
|
<span class="symbol">if</span> <span class="symbol">not</span> cmd:
|
|
sys.stdout.write<span class="paren1">(<span class="code">_cyan<span class="paren2">(<span class="code"><span class="string">"Command to run: %s/bin/"</span> % env.venv_path.rstrip<span class="paren3">(<span class="code"><span class="string">'/'</span></span>)</span></span>)</span></span>)</span>
|
|
cmd = raw_input<span class="paren1">(<span class="code"></span>)</span>.strip<span class="paren1">(<span class="code"></span>)</span>
|
|
|
|
<span class="symbol">if</span> cmd:
|
|
with cd<span class="paren1">(<span class="code">env.site_path</span>)</span>:
|
|
run<span class="paren1">(<span class="code">env.venv_path.rstrip<span class="paren2">(<span class="code"><span class="string">'/'</span></span>)</span> + <span class="string">'/bin/'</span> + cmd</span>)</span>
|
|
|
|
<span class="special">def</span><span class="keyword"> vsdo</span><span class="paren1">(<span class="code">cmd=<span class="string">""</span></span>)</span>:
|
|
<span class="string">'''Sudo a virtualenv-based command in the site directory. Usable from other commands or the CLI.'''</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'site_path'</span></span>)</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'venv_path'</span></span>)</span>
|
|
|
|
<span class="symbol">if</span> <span class="symbol">not</span> cmd:
|
|
sys.stdout.write<span class="paren1">(<span class="code">_cyan<span class="paren2">(<span class="code"><span class="string">"Command to run: sudo %s/bin/"</span> % env.venv_path.rstrip<span class="paren3">(<span class="code"><span class="string">'/'</span></span>)</span></span>)</span></span>)</span>
|
|
cmd = raw_input<span class="paren1">(<span class="code"></span>)</span>.strip<span class="paren1">(<span class="code"></span>)</span>
|
|
|
|
<span class="symbol">if</span> cmd:
|
|
with cd<span class="paren1">(<span class="code">env.site_path</span>)</span>:
|
|
sudo<span class="paren1">(<span class="code">env.venv_path.rstrip<span class="paren2">(<span class="code"><span class="string">'/'</span></span>)</span> + <span class="string">'/bin/'</span> + cmd</span>)</span></span></code></pre>
|
|
|
|
<p>This next set is just some common commands that I need to run often.</p>
|
|
|
|
<pre><code><span class="code"><span class="special">def</span><span class="keyword"> syncdb</span><span class="paren1">(<span class="code"></span>)</span>:
|
|
<span class="string">'''Run syncdb.'''</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'site_path'</span></span>)</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'venv_path'</span></span>)</span>
|
|
|
|
with cd<span class="paren1">(<span class="code">env.site_path</span>)</span>:
|
|
run<span class="paren1">(<span class="code">_python<span class="paren2">(<span class="code"><span class="string">'manage.py syncdb --noinput'</span></span>)</span></span>)</span>
|
|
|
|
<span class="special">def</span><span class="keyword"> collectstatic</span><span class="paren1">(<span class="code"></span>)</span>:
|
|
<span class="string">'''Collect static media.'''</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'site_path'</span></span>)</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'venv_path'</span></span>)</span>
|
|
|
|
with cd<span class="paren1">(<span class="code">env.site_path</span>)</span>:
|
|
sudo<span class="paren1">(<span class="code">_python<span class="paren2">(<span class="code"><span class="string">'manage.py collectstatic --noinput'</span></span>)</span></span>)</span>
|
|
|
|
<span class="special">def</span><span class="keyword"> rebuild_index</span><span class="paren1">(<span class="code"></span>)</span>:
|
|
<span class="string">'''Rebuild the search index.'''</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'site_path'</span></span>)</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'venv_path'</span></span>)</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'process_owner'</span></span>)</span>
|
|
|
|
with cd<span class="paren1">(<span class="code">env.site_path</span>)</span>:
|
|
sudo<span class="paren1">(<span class="code">_python<span class="paren2">(<span class="code"><span class="string">'manage.py rebuild_index'</span></span>)</span></span>)</span>
|
|
sudo<span class="paren1">(<span class="code"><span class="string">'chown -R %s .xapian'</span> % env.process_owner</span>)</span>
|
|
|
|
<span class="special">def</span><span class="keyword"> update_index</span><span class="paren1">(<span class="code"></span>)</span>:
|
|
<span class="string">'''Update the search index.'''</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'site_path'</span></span>)</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'venv_path'</span></span>)</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'process_owner'</span></span>)</span>
|
|
|
|
with cd<span class="paren1">(<span class="code">env.site_path</span>)</span>:
|
|
sudo<span class="paren1">(<span class="code">_python<span class="paren2">(<span class="code"><span class="string">'manage.py update_index'</span></span>)</span></span>)</span>
|
|
sudo<span class="paren1">(<span class="code"><span class="string">'chown -R %s .xapian'</span> % env.process_owner</span>)</span></span></code></pre>
|
|
|
|
<p>We also use Fabric to automate some of the more complex things we need to do.</p>
|
|
|
|
<p>This task <code>curl</code>'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.</p>
|
|
|
|
<pre><code><span class="code"><span class="special">def</span><span class="keyword"> check</span><span class="paren1">(<span class="code"></span>)</span>:
|
|
<span class="string">'''Check that the home page of the site returns an HTTP 200.'''</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'site_url'</span></span>)</span>
|
|
|
|
<span class="symbol">print</span><span class="paren1">(<span class="code"><span class="string">'Checking site status...'</span></span>)</span>
|
|
|
|
<span class="symbol">if</span> <span class="symbol">not</span> <span class="string">'200 OK'</span> <span class="symbol">in</span> local<span class="paren1">(<span class="code"><span class="string">'curl --silent -I "%s"'</span> % env.site_url, capture=True</span>)</span>:
|
|
_sad<span class="paren1">(<span class="code"></span>)</span>
|
|
else:
|
|
_happy<span class="paren1">(<span class="code"></span>)</span></span></code></pre>
|
|
|
|
<p>The <code>_happy</code> and <code>_sad</code> functions just print out some simple messages to get our
|
|
attention:</p>
|
|
|
|
<pre><code><span class="code"><span class="symbol">from</span> fabric.colors <span class="symbol">import</span> red, green
|
|
|
|
<span class="special">def</span><span class="keyword"> _happy</span><span class="paren1">(<span class="code"></span>)</span>:
|
|
<span class="symbol">print</span><span class="paren1">(<span class="code">green<span class="paren2">(<span class="code"><span class="string">'</span><span class="string">\n</span><span class="string">Looks good from here!</span><span class="string">\n</span><span class="string">'</span></span>)</span></span>)</span>
|
|
|
|
<span class="special">def</span><span class="keyword"> _sad</span><span class="paren1">(<span class="code"></span>)</span>:
|
|
<span class="symbol">print</span><span class="paren1">(<span class="code">red<span class="paren2">(<span class="code">r<span class="string">'''
|
|
___ ___
|
|
/ /</span><span class="string">\ </span><span class="string"> /__/</span><span class="string">\
|
|
</span><span class="string"> / /::</span><span class="string">\ </span><span class="string"> </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\
|
|
</span><span class="string"> / /:/</span><span class="string">\:</span><span class="string">\ </span><span class="string"> </span><span class="string">\_</span><span class="string">_</span><span class="string">\:</span><span class="string">\
|
|
</span><span class="string"> / /:/ </span><span class="string">\:</span><span class="string">\ </span><span class="string"> ___ / /::</span><span class="string">\
|
|
</span><span class="string"> /__/:/ </span><span class="string">\_</span><span class="string">_</span><span class="string">\:</span><span class="string">\ </span><span class="string">/__/</span><span class="string">\ </span><span class="string"> /:/</span><span class="string">\:</span><span class="string">\
|
|
</span><span class="string"> </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\ </span><span class="string">/ /:/ </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\/</span><span class="string">:/__</span><span class="string">\/</span><span class="string">
|
|
</span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\ </span><span class="string"> /:/ </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">:/
|
|
</span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\/</span><span class="string">:/ </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\
|
|
</span><span class="string"> </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">:/ </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\
|
|
</span><span class="string"> </span><span class="string">\_</span><span class="string">_</span><span class="string">\/</span><span class="string"> </span><span class="string">\_</span><span class="string">_</span><span class="string">\/</span><span class="string">
|
|
___ ___ ___ ___
|
|
/__/</span><span class="string">\ </span><span class="string"> / /</span><span class="string">\ </span><span class="string"> / /</span><span class="string">\ </span><span class="string"> / /</span><span class="string">\ </span><span class="string"> ___
|
|
</span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\ </span><span class="string"> / /::</span><span class="string">\ </span><span class="string"> / /:/_ / /:/_ /__/</span><span class="string">\
|
|
</span><span class="string"> </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\ </span><span class="string"> / /:/</span><span class="string">\:</span><span class="string">\ </span><span class="string"> / /:/ /</span><span class="string">\ </span><span class="string"> / /:/ /</span><span class="string">\ </span><span class="string"> </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\
|
|
</span><span class="string"> _____</span><span class="string">\_</span><span class="string">_</span><span class="string">\:</span><span class="string">\ </span><span class="string"> / /:/ </span><span class="string">\:</span><span class="string">\ </span><span class="string"> / /:/ /:/_ / /:/ /::</span><span class="string">\ </span><span class="string"> </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\
|
|
</span><span class="string"> /__/::::::::</span><span class="string">\ </span><span class="string">/__/:/ </span><span class="string">\_</span><span class="string">_</span><span class="string">\:</span><span class="string">\ </span><span class="string">/__/:/ /:/ /</span><span class="string">\ </span><span class="string">/__/:/ /:/</span><span class="string">\:</span><span class="string">\ </span><span class="string"> </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\
|
|
</span><span class="string"> </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\~</span><span class="string">~</span><span class="string">\~</span><span class="string">~</span><span class="string">\/</span><span class="string"> </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\ </span><span class="string">/ /:/ </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\/</span><span class="string">:/ /:/ </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\/</span><span class="string">:/~/:/ </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\
|
|
</span><span class="string"> </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\ </span><span class="string"> ~~~ </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\ </span><span class="string"> /:/ </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">:/ /:/ </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">:/ /:/ </span><span class="string">\_</span><span class="string">_</span><span class="string">\/</span><span class="string">
|
|
</span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\ </span><span class="string"> </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\/</span><span class="string">:/ </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\/</span><span class="string">:/ </span><span class="string">\_</span><span class="string">_</span><span class="string">\/</span><span class="string"> /:/ __
|
|
</span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">\ </span><span class="string"> </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">:/ </span><span class="string">\ </span><span class="string"> </span><span class="string">\:</span><span class="string">:/ /__/:/ /__/</span><span class="string">\
|
|
</span><span class="string"> </span><span class="string">\_</span><span class="string">_</span><span class="string">\/</span><span class="string"> </span><span class="string">\_</span><span class="string">_</span><span class="string">\/</span><span class="string"> </span><span class="string">\_</span><span class="string">_</span><span class="string">\/</span><span class="string"> </span><span class="string">\_</span><span class="string">_</span><span class="string">\/</span><span class="string"> </span><span class="string">\_</span><span class="string">_</span><span class="string">\/</span><span class="string">
|
|
|
|
|
|
Something seems to have gone wrong!
|
|
You should probably take a look at that.
|
|
'''</span></span>)</span></span>)</span></span></code></pre>
|
|
|
|
<p>This one is for when <code>python manage.py reset APP</code> is broken because you've changed
|
|
some <code>db_column</code> names and Django chokes because of some constraits and you just want
|
|
to <strong>reset the fucking app</strong>.</p>
|
|
|
|
<p>It's the "NUKE IT FROM ORBIT!!" option.</p>
|
|
|
|
<pre><code><span class="code"><span class="special">def</span><span class="keyword"> KILL_IT_WITH_FIRE</span><span class="paren1">(<span class="code">app</span>)</span>:
|
|
require<span class="paren1">(<span class="code"><span class="string">'site_path'</span></span>)</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'venv_path'</span></span>)</span>
|
|
|
|
with cd<span class="paren1">(<span class="code">env.site_path</span>)</span>:
|
|
<span class="comment"># Generate and download the reset SQL.
|
|
</span> sudo<span class="paren1">(<span class="code">_python<span class="paren2">(<span class="code"><span class="string">'manage.py sqlreset %s > reset.orig.sql'</span> % app</span>)</span></span>)</span>
|
|
get<span class="paren1">(<span class="code"><span class="string">'reset.orig.sql'</span></span>)</span>
|
|
|
|
with open<span class="paren1">(<span class="code"><span class="string">'reset.sql'</span>, <span class="string">'w'</span></span>)</span> as f:
|
|
with open<span class="paren1">(<span class="code"><span class="string">'reset.orig.sql'</span></span>)</span> as orig:
|
|
<span class="comment"># Step through the first chunk of the file (the "drop" part).
|
|
</span> line = orig.readline<span class="paren1">(<span class="code"></span>)</span>
|
|
<span class="symbol">while</span> <span class="symbol">not</span> line.startswith<span class="paren1">(<span class="code"><span class="string">'CREATE'</span></span>)</span>:
|
|
<span class="symbol">if</span> <span class="string">'CONSTRAINT'</span> <span class="symbol">in</span> line:
|
|
<span class="comment"># Don't write out CONSTRAINT lines.
|
|
</span> <span class="comment"># They're a problem when you change db_colum names.
|
|
</span> <span class="symbol">pass</span>
|
|
<span class="symbol">elif</span> <span class="string">'DROP TABLE'</span> <span class="symbol">in</span> line:
|
|
<span class="comment"># Cascade drops.
|
|
</span> <span class="comment"># Hence with "with fire" part of this task's name.
|
|
</span> line = line<span class="paren1">[<span class="code">:-2</span>]</span> + <span class="string">' CASCADE;</span><span class="string">\n</span><span class="string">'</span>
|
|
f.write<span class="paren1">(<span class="code">line</span>)</span>
|
|
else:
|
|
<span class="comment"># Write other lines through untoched.
|
|
</span> f.write<span class="paren1">(<span class="code">line</span>)</span>
|
|
line = orig.readline<span class="paren1">(<span class="code"></span>)</span>
|
|
|
|
<span class="comment"># Write out the rest of the file untouched.
|
|
</span> f.write<span class="paren1">(<span class="code">line</span>)</span>
|
|
f.write<span class="paren1">(<span class="code">orig.read<span class="paren2">(<span class="code"></span>)</span></span>)</span>
|
|
|
|
<span class="comment"># Upload the processed SQL file.
|
|
</span> put<span class="paren1">(<span class="code"><span class="string">'reset.sql'</span>, os.path.join<span class="paren2">(<span class="code">env.site_path, <span class="string">'reset.ready.sql'</span></span>)</span>, use_sudo=True</span>)</span>
|
|
|
|
with cd<span class="paren1">(<span class="code">env.site_path</span>)</span>:
|
|
<span class="comment"># Use the SQL to reset the app, and fake a migration.
|
|
</span> run<span class="paren1">(<span class="code">_python<span class="paren2">(<span class="code"><span class="string">'manage.py dbshell < reset.ready.sql'</span></span>)</span></span>)</span>
|
|
sudo<span class="paren1">(<span class="code">_python<span class="paren2">(<span class="code"><span class="string">'manage.py migrate --fake --delete-ghost-migrations '</span> + app</span>)</span></span>)</span></span></code></pre>
|
|
|
|
<p>This task uses Mercurial's local tags to add a <code>production</code> or <code>staging</code> tag in your local
|
|
repository, so you can easy see where the production/staging servers are at
|
|
compared to your local repo.</p>
|
|
|
|
<pre><code><span class="code"><span class="special">def</span><span class="keyword"> retag</span><span class="paren1">(<span class="code"></span>)</span>:
|
|
<span class="string">'''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.
|
|
'''</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'site_path'</span>, provided_by=<span class="paren2">[<span class="code"><span class="string">'prod'</span>, <span class="string">'stag'</span></span>]</span></span>)</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'env_name'</span>, provided_by=<span class="paren2">[<span class="code"><span class="string">'prod'</span>, <span class="string">'stag'</span></span>]</span></span>)</span>
|
|
|
|
with cd<span class="paren1">(<span class="code">env.site_path</span>)</span>:
|
|
current = run<span class="paren1">(<span class="code"><span class="string">'hg id --rev . --quiet'</span></span>)</span>.strip<span class="paren1">(<span class="code"><span class="string">' </span><span class="string">\n</span><span class="string">+'</span></span>)</span>
|
|
|
|
local<span class="paren1">(<span class="code"><span class="string">'hg tag --local --force %s --rev %s'</span> % <span class="paren2">(<span class="code">env.env_name, current</span>)</span></span>)</span></span></code></pre>
|
|
|
|
<p>This task tails the Gunicorn logs on the server so you can quickly find out what's
|
|
happening when things blow up.</p>
|
|
|
|
<pre><code><span class="code"><span class="special">def</span><span class="keyword"> tailgun</span><span class="paren1">(<span class="code">follow=<span class="string">''</span></span>)</span>:
|
|
<span class="string">""</span><span class="string">"Tail the Gunicorn log file."</span><span class="string">""</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'site_path'</span></span>)</span>
|
|
|
|
with cd<span class="paren1">(<span class="code">env.site_path</span>)</span>:
|
|
<span class="symbol">if</span> follow:
|
|
run<span class="paren1">(<span class="code"><span class="string">'tail -f .gunicorn.log'</span></span>)</span>
|
|
else:
|
|
run<span class="paren1">(<span class="code"><span class="string">'tail .gunicorn.log'</span></span>)</span></span></code></pre>
|
|
|
|
<p>We've got a lot of other tasks but they're pretty specific to our setup.</p>
|
|
|
|
<h2 id="s4-wrangling-databases-with-south"><a href="index.html#s4-wrangling-databases-with-south">Wrangling Databases with South</a></h2>
|
|
|
|
<p>If you're not using <a href="http://south.aeracode.org/">South</a>, you need to start. Now.</p>
|
|
|
|
<p>No, really, I'll wait. Take 30 minutes, try the <a href="http://south.aeracode.org/docs/tutorial/index.html">tutorial</a>, wrap your head
|
|
around it and come back. It's far more important than this blog post.</p>
|
|
|
|
<h3 id="s5-useful-fabric-tasks"><a href="index.html#s5-useful-fabric-tasks">Useful Fabric Tasks</a></h3>
|
|
|
|
<p>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:</p>
|
|
|
|
<pre><code><span class="code"><span class="special">def</span><span class="keyword"> migrate</span><span class="paren1">(<span class="code">args=<span class="string">''</span></span>)</span>:
|
|
<span class="string">'''Run any needed migrations.'''</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'site_path'</span></span>)</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'venv_path'</span></span>)</span>
|
|
|
|
with cd<span class="paren1">(<span class="code">env.site_path</span>)</span>:
|
|
sudo<span class="paren1">(<span class="code">_python<span class="paren2">(<span class="code"><span class="string">'manage.py migrate '</span> + args</span>)</span></span>)</span>
|
|
|
|
<span class="special">def</span><span class="keyword"> migrate_fake</span><span class="paren1">(<span class="code">args=<span class="string">''</span></span>)</span>:
|
|
<span class="string">'''Run any needed migrations with --fake.'''</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'site_path'</span></span>)</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'venv_path'</span></span>)</span>
|
|
|
|
with cd<span class="paren1">(<span class="code">env.site_path</span>)</span>:
|
|
sudo<span class="paren1">(<span class="code">_python<span class="paren2">(<span class="code"><span class="string">'manage.py migrate --fake '</span> + args</span>)</span></span>)</span>
|
|
|
|
<span class="special">def</span><span class="keyword"> migrate_reset</span><span class="paren1">(<span class="code">args=<span class="string">''</span></span>)</span>:
|
|
<span class="string">'''Run any needed migrations with --fake. No, seriously.'''</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'site_path'</span></span>)</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'venv_path'</span></span>)</span>
|
|
|
|
with cd<span class="paren1">(<span class="code">env.site_path</span>)</span>:
|
|
sudo<span class="paren1">(<span class="code">_python<span class="paren2">(<span class="code"><span class="string">'manage.py migrate --fake --delete-ghost-migrations '</span> + args</span>)</span></span>)</span></span></code></pre>
|
|
|
|
<p>Remember that running a migration without specifying an app will migrate everything,
|
|
so a simple <code>fab dev migrate</code> will do the trick.</p>
|
|
|
|
<h2 id="s6-watching-for-changes"><a href="index.html#s6-watching-for-changes">Watching for Changes</a></h2>
|
|
|
|
<p>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.</p>
|
|
|
|
<p>First, add a <code>monitor.py</code> file at the root of your project (I believe I found this
|
|
code <a href="http://code.google.com/p/modwsgi/wiki/ReloadingSourceCode">here</a>, but I may be wrong):</p>
|
|
|
|
<pre><code><span class="code"><span class="symbol">import</span> os
|
|
<span class="symbol">import</span> sys
|
|
<span class="symbol">import</span> time
|
|
<span class="symbol">import</span> signal
|
|
<span class="symbol">import</span> threading
|
|
<span class="symbol">import</span> atexit
|
|
<span class="symbol">import</span> Queue
|
|
|
|
_interval = 1.0
|
|
_times = <span class="paren1">{<span class="code"></span>}</span>
|
|
_files = <span class="paren1">[<span class="code"></span>]</span>
|
|
|
|
_running = False
|
|
_queue = Queue.Queue<span class="paren1">(<span class="code"></span>)</span>
|
|
_lock = threading.Lock<span class="paren1">(<span class="code"></span>)</span>
|
|
|
|
<span class="special">def</span><span class="keyword"> _restart</span><span class="paren1">(<span class="code">path</span>)</span>:
|
|
_queue.put<span class="paren1">(<span class="code">True</span>)</span>
|
|
prefix = <span class="string">'monitor (pid=%d):'</span> % os.getpid<span class="paren1">(<span class="code"></span>)</span>
|
|
<span class="symbol">print</span> >> sys.stderr, <span class="string">'%s Change detected to </span><span class="string">\'</span><span class="string">%s</span><span class="string">\'</span><span class="string">.'</span> % <span class="paren1">(<span class="code">prefix, path</span>)</span>
|
|
<span class="symbol">print</span> >> sys.stderr, <span class="string">'%s Triggering process restart.'</span> % prefix
|
|
os.kill<span class="paren1">(<span class="code">os.getpid<span class="paren2">(<span class="code"></span>)</span>, signal.SIGINT</span>)</span>
|
|
|
|
<span class="special">def</span><span class="keyword"> _modified</span><span class="paren1">(<span class="code">path</span>)</span>:
|
|
try:
|
|
<span class="comment"># If path doesn't denote a file and were previously
|
|
</span> <span class="comment"># tracking it, then it has been removed or the file type
|
|
</span> <span class="comment"># has changed so force a restart. If not previously
|
|
</span> <span class="comment"># tracking the file then we can ignore it as probably
|
|
</span> <span class="comment"># pseudo reference such as when file extracted from a
|
|
</span> <span class="comment"># collection of modules contained in a zip file.
|
|
</span>
|
|
<span class="symbol">if</span> <span class="symbol">not</span> os.path.isfile<span class="paren1">(<span class="code">path</span>)</span>:
|
|
<span class="symbol">return</span> path <span class="symbol">in</span> _times
|
|
|
|
<span class="comment"># Check for when file last modified.
|
|
</span>
|
|
mtime = os.stat<span class="paren1">(<span class="code">path</span>)</span>.st_mtime
|
|
<span class="symbol">if</span> path <span class="symbol">not</span> <span class="symbol">in</span> _times:
|
|
_times<span class="paren1">[<span class="code">path</span>]</span> = mtime
|
|
|
|
<span class="comment"># Force restart when modification time has changed, even
|
|
</span> <span class="comment"># if time now older, as that could indicate older file
|
|
</span> <span class="comment"># has been restored.
|
|
</span>
|
|
<span class="symbol">if</span> mtime != _times<span class="paren1">[<span class="code">path</span>]</span>:
|
|
<span class="symbol">return</span> True
|
|
except:
|
|
<span class="comment"># If any exception occured, likely that file has been
|
|
</span> <span class="comment"># been removed just before stat(), so force a restart.
|
|
</span>
|
|
<span class="symbol">return</span> True
|
|
|
|
<span class="symbol">return</span> False
|
|
|
|
<span class="special">def</span><span class="keyword"> _monitor</span><span class="paren1">(<span class="code"></span>)</span>:
|
|
<span class="symbol">while</span> 1:
|
|
<span class="comment"># Check modification times on all files in sys.modules.
|
|
</span>
|
|
<span class="symbol">for</span> module <span class="symbol">in</span> sys.modules.values<span class="paren1">(<span class="code"></span>)</span>:
|
|
<span class="symbol">if</span> <span class="symbol">not</span> hasattr<span class="paren1">(<span class="code">module, <span class="string">'__file__'</span></span>)</span>:
|
|
<span class="symbol">continue</span>
|
|
path = getattr<span class="paren1">(<span class="code">module, <span class="string">'__file__'</span></span>)</span>
|
|
<span class="symbol">if</span> <span class="symbol">not</span> path:
|
|
<span class="symbol">continue</span>
|
|
<span class="symbol">if</span> os.path.splitext<span class="paren1">(<span class="code">path</span>)</span><span class="paren1">[<span class="code">1</span>]</span> <span class="symbol">in</span> <span class="paren1">[<span class="code"><span class="string">'.pyc'</span>, <span class="string">'.pyo'</span>, <span class="string">'.pyd'</span></span>]</span>:
|
|
path = path<span class="paren1">[<span class="code">:-1</span>]</span>
|
|
<span class="symbol">if</span> _modified<span class="paren1">(<span class="code">path</span>)</span>:
|
|
<span class="symbol">return</span> _restart<span class="paren1">(<span class="code">path</span>)</span>
|
|
|
|
<span class="comment"># Check modification times on files which have
|
|
</span> <span class="comment"># specifically been registered for monitoring.
|
|
</span>
|
|
<span class="symbol">for</span> path <span class="symbol">in</span> _files:
|
|
<span class="symbol">if</span> _modified<span class="paren1">(<span class="code">path</span>)</span>:
|
|
<span class="symbol">return</span> _restart<span class="paren1">(<span class="code">path</span>)</span>
|
|
|
|
<span class="comment"># Go to sleep for specified interval.
|
|
</span>
|
|
try:
|
|
<span class="symbol">return</span> _queue.get<span class="paren1">(<span class="code">timeout=_interval</span>)</span>
|
|
except:
|
|
<span class="symbol">pass</span>
|
|
|
|
_thread = threading.Thread<span class="paren1">(<span class="code">target=_monitor</span>)</span>
|
|
_thread.setDaemon<span class="paren1">(<span class="code">True</span>)</span>
|
|
|
|
<span class="special">def</span><span class="keyword"> _exiting</span><span class="paren1">(<span class="code"></span>)</span>:
|
|
try:
|
|
_queue.put<span class="paren1">(<span class="code">True</span>)</span>
|
|
except:
|
|
<span class="symbol">pass</span>
|
|
_thread.join<span class="paren1">(<span class="code"></span>)</span>
|
|
|
|
atexit.register<span class="paren1">(<span class="code">_exiting</span>)</span>
|
|
|
|
<span class="special">def</span><span class="keyword"> track</span><span class="paren1">(<span class="code">path</span>)</span>:
|
|
<span class="symbol">if</span> <span class="symbol">not</span> path <span class="symbol">in</span> _files:
|
|
_files.append<span class="paren1">(<span class="code">path</span>)</span>
|
|
|
|
<span class="special">def</span><span class="keyword"> start</span><span class="paren1">(<span class="code">interval=1.0</span>)</span>:
|
|
<span class="symbol">global</span> _interval
|
|
<span class="symbol">if</span> interval < _interval:
|
|
_interval = interval
|
|
|
|
<span class="symbol">global</span> _running
|
|
_lock.acquire<span class="paren1">(<span class="code"></span>)</span>
|
|
<span class="symbol">if</span> <span class="symbol">not</span> _running:
|
|
prefix = <span class="string">'monitor (pid=%d):'</span> % os.getpid<span class="paren1">(<span class="code"></span>)</span>
|
|
<span class="symbol">print</span> >> sys.stderr, <span class="string">'%s Starting change monitor.'</span> % prefix
|
|
_running = True
|
|
_thread.start<span class="paren1">(<span class="code"></span>)</span>
|
|
_lock.release<span class="paren1">(<span class="code"></span>)</span></span></code></pre>
|
|
|
|
<p>Next add a <code>post_fork</code> hook to your Gunicorn config file that uses the monitor to
|
|
watch for changes:</p>
|
|
|
|
<pre><code><span class="code"><span class="special">def</span><span class="keyword"> post_fork</span><span class="paren1">(<span class="code">server, worker</span>)</span>:
|
|
<span class="symbol">import</span> monitor
|
|
<span class="symbol">import</span> local_settings
|
|
<span class="symbol">if</span> local_settings.DEBUG:
|
|
server.log.info<span class="paren1">(<span class="code"><span class="string">"Starting change monitor."</span></span>)</span>
|
|
monitor.start<span class="paren1">(<span class="code">interval=1.0</span>)</span></span></code></pre>
|
|
|
|
<p>Now the Gunicorn server will automatically restart whenever code is changed. Use
|
|
whatever method for determining debug status that you like. We use
|
|
<code>local_settings.py</code> files which all have <code>DEBUG</code> variables, so that works for us.</p>
|
|
|
|
<p>It will <em>not</em> restart when you add new code (e.g. when you install a new app), so
|
|
you'll need to handle that manually with <code>fab dev restart</code>, but that's not too bad!</p>
|
|
|
|
<h3 id="s7-using-the-werkzeug-debugger-with-gunicorn"><a href="index.html#s7-using-the-werkzeug-debugger-with-gunicorn">Using the Werkzeug Debugger with Gunicorn</a></h3>
|
|
|
|
<p>The final piece of the puzzle is being able to use the fantastic <a href="http://werkzeug.pocoo.org/docs/debug/">Werkzeug
|
|
Debugger</a> while running on the development VM with Gunicorn.</p>
|
|
|
|
<p>To do this, create a <code>debug_wsgi.py</code> file at the root of your project:</p>
|
|
|
|
<pre><code><span class="code"><span class="symbol">import</span> os
|
|
<span class="symbol">import</span> sys
|
|
<span class="symbol">import</span> site
|
|
|
|
parent = os.path.dirname
|
|
site_dir = parent<span class="paren1">(<span class="code">os.path.abspath<span class="paren2">(<span class="code">__file__</span>)</span></span>)</span>
|
|
project_dir = parent<span class="paren1">(<span class="code">parent<span class="paren2">(<span class="code">os.path.abspath<span class="paren3">(<span class="code">__file__</span>)</span></span>)</span></span>)</span>
|
|
|
|
sys.path.insert<span class="paren1">(<span class="code">0, project_dir</span>)</span>
|
|
sys.path.insert<span class="paren1">(<span class="code">0, site_dir</span>)</span>
|
|
|
|
site.addsitedir<span class="paren1">(<span class="code"><span class="string">'VIRTUALENV_SITE_PACKAGES'</span></span>)</span>
|
|
|
|
<span class="symbol">from</span> django.core.management <span class="symbol">import</span> setup_environ
|
|
<span class="symbol">import</span> settings
|
|
setup_environ<span class="paren1">(<span class="code">settings</span>)</span>
|
|
|
|
<span class="symbol">import</span> django.core.handlers.wsgi
|
|
application = django.core.handlers.wsgi.WSGIHandler<span class="paren1">(<span class="code"></span>)</span>
|
|
|
|
<span class="symbol">from</span> werkzeug.debug <span class="symbol">import</span> DebuggedApplication
|
|
application = DebuggedApplication<span class="paren1">(<span class="code">application, evalex=True</span>)</span>
|
|
|
|
<span class="special">def</span><span class="keyword"> null_technical_500_response</span><span class="paren1">(<span class="code">request, exc_type, exc_value, tb</span>)</span>:
|
|
<span class="symbol">raise</span> exc_type, exc_value, tb
|
|
<span class="symbol">from</span> django.views <span class="symbol">import</span> debug
|
|
debug.technical_500_response = null_technical_500_response</span></code></pre>
|
|
|
|
<p>Have Gunicorn use this file to run your development server with <code>gunicorn
|
|
debug_wsgi:application</code>.</p>
|
|
|
|
<p>Make sure to replace <code>'VIRTUALENV_SITE_PACKAGES'</code> with the <em>full</em> path to your
|
|
virtualenv's <code>site_packages</code> directory. You might want to make this a setting in
|
|
a machine-specific settings file.</p>
|
|
|
|
<h3 id="s8-pulling-uploads"><a href="index.html#s8-pulling-uploads">Pulling Uploads</a></h3>
|
|
|
|
<p>Once you give a client access to a site they'll probably be uploading images (through
|
|
Django's built-in file uploading features or with <a href="http://code.google.com/p/django-filebrowser/">django-filebrowser</a>).</p>
|
|
|
|
<p>When you're making changes locally it's often useful to have these uploaded files on
|
|
your local VM, otherwise you end up with a bunch of broken images.</p>
|
|
|
|
<p>Here's a simple Fabric task that will pull down all the uploads from the server:</p>
|
|
|
|
<pre><code><span class="code"><span class="special">def</span><span class="keyword"> pull_uploads</span><span class="paren1">(<span class="code"></span>)</span>:
|
|
<span class="string">'''Copy the uploads from the site to your local machine.'''</span>
|
|
require<span class="paren1">(<span class="code"><span class="string">'uploads_path'</span></span>)</span>
|
|
|
|
sudo<span class="paren1">(<span class="code"><span class="string">'chmod -R a+r "%s"'</span> % env.uploads_path</span>)</span>
|
|
|
|
rsync_command = r<span class="string">""</span><span class="string">"rsync -av -e 'ssh -p %s' %s@%s:%s %s"</span><span class="string">""</span> % <span class="paren1">(<span class="code">
|
|
env.port,
|
|
env.user, env.host,
|
|
env.uploads_path.rstrip<span class="paren2">(<span class="code"><span class="string">'/'</span></span>)</span> + <span class="string">'/'</span>,
|
|
<span class="string">'media/uploads'</span>
|
|
</span>)</span>
|
|
<span class="symbol">print</span> local<span class="paren1">(<span class="code">rsync_command, capture=False</span>)</span></span></code></pre>
|
|
|
|
<p>You might be wondering about the line that strips <code>/</code> characters and then adds them
|
|
back in. <code>rsync</code> does different things depending on whether you end a path with
|
|
a <code>/</code>, so this is actually pretty important.</p>
|
|
|
|
<p>In your host task you'll need to set the <code>uploads_path</code> variable to something like
|
|
this:</p>
|
|
|
|
<pre><code><span class="code"><span class="symbol">import</span> os
|
|
env.site_path = os.path.join<span class="paren1">(<span class="code"><span class="string">'var'</span>, <span class="string">'www'</span>, <span class="string">'myproject'</span></span>)</span>
|
|
env.uploads_path = os.path.join<span class="paren1">(<span class="code">env.site_path, <span class="string">'media'</span>, <span class="string">'uploads'</span></span>)</span></span></code></pre>
|
|
|
|
<p>Now you can run <code>fab production pull_uploads</code> to pull down all the files people have
|
|
uploaded to the production server.</p>
|
|
|
|
<h3 id="s9-preventing-accidents"><a href="index.html#s9-preventing-accidents">Preventing Accidents</a></h3>
|
|
|
|
<p>Deploying to test and staging servers should be quick and easy. Deploying to
|
|
production servers should be harder to prevent people from accidentally doing it.</p>
|
|
|
|
<p>I've created a little function that I call before deploying to production servers.
|
|
It forces me to type in random words from the system word list before proceeding to
|
|
make sure I <em>really</em> know what I'm doing:</p>
|
|
|
|
<pre><code><span class="code"><span class="symbol">import</span> os, random
|
|
|
|
<span class="symbol">from</span> fabric.api <span class="symbol">import</span> *
|
|
<span class="symbol">from</span> fabric.operations <span class="symbol">import</span> prompt
|
|
<span class="symbol">from</span> fabric.utils <span class="symbol">import</span> abort
|
|
|
|
WORDLIST_PATHS = <span class="paren1">[<span class="code">os.path.join<span class="paren2">(<span class="code"><span class="string">'/'</span>, <span class="string">'usr'</span>, <span class="string">'share'</span>, <span class="string">'dict'</span>, <span class="string">'words'</span></span>)</span></span>]</span>
|
|
DEFAULT_MESSAGE = <span class="string">"Are you sure you want to do this?"</span>
|
|
WORD_PROMPT = <span class="string">' [%d/%d] Type "%s" to continue (^C quits): '</span>
|
|
|
|
<span class="special">def</span><span class="keyword"> prevent_horrible_accidents</span><span class="paren1">(<span class="code">msg=DEFAULT_MESSAGE, horror_rating=1</span>)</span>:
|
|
<span class="string">""</span><span class="string">"Prompt the user to enter random words to prevent doing something stupid."</span><span class="string">""</span>
|
|
|
|
valid_wordlist_paths = <span class="paren1">[<span class="code">wp <span class="symbol">for</span> wp <span class="symbol">in</span> WORDLIST_PATHS <span class="symbol">if</span> os.path.exists<span class="paren2">(<span class="code">wp</span>)</span></span>]</span>
|
|
|
|
<span class="symbol">if</span> <span class="symbol">not</span> valid_wordlist_paths:
|
|
abort<span class="paren1">(<span class="code"><span class="string">'No wordlists found!'</span></span>)</span>
|
|
|
|
with open<span class="paren1">(<span class="code">valid_wordlist_paths<span class="paren2">[<span class="code">0</span>]</span></span>)</span> as wordlist_file:
|
|
words = wordlist_file.readlines<span class="paren1">(<span class="code"></span>)</span>
|
|
|
|
<span class="symbol">print</span> msg
|
|
|
|
<span class="symbol">for</span> i <span class="symbol">in</span> range<span class="paren1">(<span class="code">int<span class="paren2">(<span class="code">horror_rating</span>)</span></span>)</span>:
|
|
word = words<span class="paren1">[<span class="code">random.randint<span class="paren2">(<span class="code">0, len<span class="paren3">(<span class="code">words</span>)</span></span>)</span></span>]</span>.strip<span class="paren1">(<span class="code"></span>)</span>
|
|
p_msg = WORD_PROMPT % <span class="paren1">(<span class="code">i+1, horror_rating, word</span>)</span>
|
|
answer = prompt<span class="paren1">(<span class="code">p_msg, validate=r<span class="string">'^%s$'</span> % word</span>)</span></span></code></pre>
|
|
|
|
<p>You may need to adjust <code>WORDLIST_PATHS</code> if you're not on OS X.</p>
|
|
|
|
<h2 id="s10-working-with-third-party-apps"><a href="index.html#s10-working-with-third-party-apps">Working with Third-Party Apps</a></h2>
|
|
|
|
<p>One of the best parts about working with Django is that many problems have already
|
|
been solved and the solutions have been released as open-source applications.</p>
|
|
|
|
<p>We use quite a few open-source apps, and there are a couple of tricks I've learned to
|
|
make working with them easier.</p>
|
|
|
|
<h3 id="s11-installing-apps-from-repositories"><a href="index.html#s11-installing-apps-from-repositories">Installing Apps from Repositories</a></h3>
|
|
|
|
<p>If I'm going to use an open-source Django app in a project I'll almost always install
|
|
it as an editable repository on the VM with <code>pip install -e</code>.</p>
|
|
|
|
<p>Others may disagree with me on this, but I think it's the best way to work.</p>
|
|
|
|
<p>Often I'll find a bug that I think may be in one of the third-party apps I'm using.
|
|
Installing the apps as repositories makes it easy to read their source and figure out
|
|
if the bug is really in the app.</p>
|
|
|
|
<p>If the bug <em>is</em> in the third-party app having the app installed as a repository makes
|
|
it simple to fix the bug, fork the project on BitBucket or GitHub, send a pull
|
|
request, and get back to work.</p>
|
|
|
|
<h3 id="s12-mirroring-repositories"><a href="index.html#s12-mirroring-repositories">Mirroring Repositories</a></h3>
|
|
|
|
<p>One problem we've run into at Dumbwaiter is that the repos for third-party apps we
|
|
use are scattered across GitHub, BitBucket, Google Code, and other servers. If any
|
|
one of these services goes down we're stuck waiting for it to come back up.</p>
|
|
|
|
<p>A while ago I took half a day and consolidated all of these repos onto one of the
|
|
servers that we control. The basic process went like this:</p>
|
|
|
|
<ul>
|
|
<li>Use <a href="http://hg-git.github.com/">hg-git</a> and <a href="https://bitbucket.org/durin42/hgsubversion/wiki/Home">hgsubversion</a> to convert the git and SVN repos to Mercurial
|
|
repos.</li>
|
|
<li>Set up a master <code>mirror</code> Mercurial repo with all the app repos as subrepos.</li>
|
|
<li>Push the master repo and all the subrepos up to one of our Linodes.</li>
|
|
</ul>
|
|
|
|
<p>Now we can use <code>-e ssh://hg@OUR_LINODE/mirror/APP@REV_THAT_WORKS#egg=APP</code> in our
|
|
<code>requirements.txt</code> files to install apps from our mirror. When we want to update our
|
|
dependencies we can simply pull from the upstream repos and commit in the mirror
|
|
repo.</p>
|
|
|
|
<p>If our mirror goes down it's not a big deal, because we have far bigger problems to
|
|
worry about than new projects.</p>
|
|
|
|
<p>I wrote a few scripts to automate updating apps and such, but they're extremely hacky
|
|
so I don't want to post them here. Take half a day and write your own set — it's
|
|
definitely worth it to have your own mirror of your specific dependencies.</p>
|
|
|
|
<h3 id="s13-using-bcvi-to-edit-files"><a href="index.html#s13-using-bcvi-to-edit-files">Using BCVI to Edit Files</a></h3>
|
|
|
|
<p>I said that when I find a bug that I think is in a third-party app I'll poke around
|
|
with the app and try to figure it out. But since all the apps are installed in
|
|
a virtualenv on the Vagrant VM it might seem like it's a pain in the ass to edit
|
|
those files!</p>
|
|
|
|
<p>Luckily <a href="http://sshmenu.sourceforge.net/articles/bcvi/">BCVI</a> exists. It's a utility that opens a "back channel" to your local
|
|
machine when you SSH and lets you run <code>vi FILE</code> to open that file in
|
|
Vim/MacVim/GVim/etc on your <em>local</em> machine. When you save the file it uploads it
|
|
back to the server automatically for you.</p>
|
|
|
|
<p>It can be a bit tricky to set up, but it's worth it. Trust me.</p>
|
|
|
|
<h2 id="s14-improving-the-admin-interface"><a href="index.html#s14-improving-the-admin-interface">Improving the Admin Interface</a></h2>
|
|
|
|
<p>I'm going to be honest: Django's admin interface is the main reason I'm still using
|
|
it. Other frameworks like <a href="http://flask.pocoo.org/">Flask</a> are great, but Django's admin saves me
|
|
<em>ridiculous</em> amounts of time when I'm making simple CRUD sites for clients.</p>
|
|
|
|
<p>That said, the Django admin isn't the prettiest thing around, but we can give it
|
|
a facelift.</p>
|
|
|
|
<h3 id="s15-enter-grappelli"><a href="index.html#s15-enter-grappelli">Enter Grappelli</a></h3>
|
|
|
|
<p><a href="http://django-grappelli.readthedocs.org/">Grappelli</a> is a Django app that reskins the admin interface beautifully. It also
|
|
adds some functionality like drag-and-drop reordering of inlines, and allows you to
|
|
customize the dashboard to your liking. <em>Every</em> Django site I work on uses Grappelli
|
|
-- it's just that good.</p>
|
|
|
|
<p>The downside of Grappelli is that it changes quite a lot and breaks backwards
|
|
compatibility at the drop of a hat.</p>
|
|
|
|
<p>If you're going to use Grappelli you <em>must</em> freeze your requirements.txt files and
|
|
work with a single version at a time. Trying to always work from the trunk will make
|
|
you drink.</p>
|
|
|
|
<h3 id="s16-an-ugly-hack-to-show-usable-foreign-key-fields"><a href="index.html#s16-an-ugly-hack-to-show-usable-foreign-key-fields">An Ugly Hack to Show Usable Foreign Key Fields</a></h3>
|
|
|
|
<p>A limitation of both Grappelli and the stock Django admin is that it seems like you
|
|
can't easily show fields from related models in the admin list view.</p>
|
|
|
|
<p>For example, if you're new to Django you might expect this to work:</p>
|
|
|
|
<pre><code><span class="code"><span class="special">class</span><span class="keyword"> BlogEntryAdmin</span><span class="paren1">(<span class="code">admin.ModelAdmin</span>)</span>:
|
|
list_display = <span class="paren1">(<span class="code"><span class="string">'title'</span>, <span class="string">'author__name'</span></span>)</span></span></code></pre>
|
|
|
|
<p>Unfortunately Django chokes on the <code>author__name</code> lookup. You can <em>display</em> the name
|
|
without too much fuss:</p>
|
|
|
|
<pre><code><span class="code"><span class="special">class</span><span class="keyword"> BlogEntryAdmin</span><span class="paren1">(<span class="code">admin.ModelAdmin</span>)</span>:
|
|
list_display = <span class="paren1">(<span class="code"><span class="string">'title'</span>, <span class="string">'author_name'</span></span>)</span>
|
|
|
|
<span class="special">def</span><span class="keyword"> author_name</span><span class="paren1">(<span class="code">self, obj</span>)</span>:
|
|
<span class="symbol">return</span> obj.name</span></code></pre>
|
|
|
|
<p>That will display the name just fine. However, it won't be a fully-fledged column in
|
|
the Django admin because you can't sort on it.</p>
|
|
|
|
<p>It may seem like this is the end — if it could be a fully-functional field, why
|
|
wouldn't Django just let you use <code>author__name</code>? Luckily we can add one more line to
|
|
fix the problem:</p>
|
|
|
|
<pre><code><span class="code"><span class="special">class</span><span class="keyword"> BlogEntryAdmin</span><span class="paren1">(<span class="code">admin.ModelAdmin</span>)</span>:
|
|
list_display = <span class="paren1">(<span class="code"><span class="string">'title'</span>, <span class="string">'author_name'</span></span>)</span>
|
|
|
|
<span class="special">def</span><span class="keyword"> author_name</span><span class="paren1">(<span class="code">self, obj</span>)</span>:
|
|
<span class="symbol">return</span> obj.name
|
|
author_name.admin_order_field = <span class="string">'author__name'</span></span></code></pre>
|
|
|
|
<p>Now the author name has all the functionality of a real <code>list_display</code> entry.</p>
|
|
|
|
<h2 id="s17-using-django-annoying"><a href="index.html#s17-using-django-annoying">Using Django-Annoying</a></h2>
|
|
|
|
<p>If you haven't heard of <a href="https://bitbucket.org/offline/django-annoying/wiki/Home">django-annoying</a> you should definitely check it out. It's
|
|
got a bunch of miscellaneous functions that fix some common, annoying parts of
|
|
Django.</p>
|
|
|
|
<p>My two personal favorites from the package are a pair of decorators that help make
|
|
your views much, much cleaner.</p>
|
|
|
|
<h3 id="s18-the-render-to-decorator"><a href="index.html#s18-the-render-to-decorator">The render_to Decorator</a></h3>
|
|
|
|
<p>The decorator is called <code>render_to</code> and it eliminates the ugly <code>render_to_response</code>
|
|
calls that Django normally forces you to use in every single view.</p>
|
|
|
|
<p>Normally you'd use something like this:</p>
|
|
|
|
<pre><code><span class="code"><span class="special">def</span><span class="keyword"> videos</span><span class="paren1">(<span class="code">request</span>)</span>:
|
|
videos = Video.objects.all<span class="paren1">(<span class="code"></span>)</span>
|
|
<span class="symbol">return</span> render_to_response<span class="paren1">(<span class="code"><span class="string">'video_list.html'</span>, <span class="paren2">{<span class="code"> <span class="string">'videos'</span>: videos </span>}</span>,
|
|
context_instance=RequestContext<span class="paren2">(<span class="code">request</span>)</span></span>)</span></span></code></pre>
|
|
|
|
<p>With <code>render_to</code> your view gets much cleaner:</p>
|
|
|
|
<pre><code><span class="code"><span class="symbol">@render_to</span><span class="paren1">(<span class="code"><span class="string">'video_list.html'</span></span>)</span>
|
|
<span class="special">def</span><span class="keyword"> videos</span><span class="paren1">(<span class="code">request</span>)</span>:
|
|
videos = Video.objects.all<span class="paren1">(<span class="code"></span>)</span>
|
|
<span class="symbol">return</span> <span class="paren1">{<span class="code"> <span class="string">'videos'</span>: videos </span>}</span></span></code></pre>
|
|
|
|
<p>Less typing <code>context_instance=...</code> over and over, and less syntax to remember.</p>
|
|
|
|
<p>Yes, I know about Django 1.3's <code>render</code> shortcut. You have to type <code>request</code> every
|
|
single time with <code>render</code>, so the <code>render_to</code> decorator still wins.</p>
|
|
|
|
<h3 id="s19-the-ajax-request-decorator"><a href="index.html#s19-the-ajax-request-decorator">The ajax_request Decorator</a></h3>
|
|
|
|
<p>The <code>ajax_request</code> decorator is like <code>render_to</code> for AJAX requests. You simply
|
|
return a Python dictionary from your view and the decorator handles the JSON encoding
|
|
and such:</p>
|
|
|
|
<pre><code><span class="code"><span class="symbol">@ajax_request</span>
|
|
<span class="special">def</span><span class="keyword"> ajax_get_entries</span><span class="paren1">(<span class="code">request</span>)</span>:
|
|
blog_entries = BlogEntry.objects.all<span class="paren1">(<span class="code"></span>)</span>
|
|
<span class="symbol">return</span> <span class="paren1">{<span class="code"> <span class="string">'entries'</span>: <span class="paren2">[<span class="code"><span class="paren3">(<span class="code">entry.title, entry.get_absolute_url<span class="paren4">(<span class="code"></span>)</span></span>)</span>
|
|
<span class="symbol">for</span> entry <span class="symbol">in</span> entries</span>]</span></span>}</span></span></code></pre>
|
|
|
|
<h2 id="s20-templating-tricks"><a href="index.html#s20-templating-tricks">Templating Tricks</a></h2>
|
|
|
|
<p>I'm not a frontend developer, but I've done my share of HTML hacking at Dumbwaiter.
|
|
Here are a few of the tricks I've learned.</p>
|
|
|
|
<h3 id="s21-null-checks-and-fallbacks"><a href="index.html#s21-null-checks-and-fallbacks">Null Checks and Fallbacks</a></h3>
|
|
|
|
<p>A common pattern I see in Django templates looks like this:</p>
|
|
|
|
<pre><code>{% templatetag openblock %} if business.title {% templatetag closeblock %}
|
|
{% templatetag openvariable %} business.title {% templatetag closevariable %}
|
|
{% templatetag openblock %} else {% templatetag closeblock %}
|
|
{% templatetag openvariable %} business.short_title {% templatetag closevariable %}
|
|
{% templatetag openblock %} endif {% templatetag closeblock %}</code></pre>
|
|
|
|
<p>Here's a simpler way to do that:</p>
|
|
|
|
<pre><code>{% templatetag openblock %} firstof business.title business.short_title {% templatetag closeblock %}</code></pre>
|
|
|
|
<p><code>firstof</code> will return the first non-Falsy item in its arguments.</p>
|
|
|
|
<h3 id="s22-manipulating-query-strings"><a href="index.html#s22-manipulating-query-strings">Manipulating Query Strings</a></h3>
|
|
|
|
<p>Query strings are normally not a big deal, but every once in a while you'll have
|
|
a model listing page where you need to filter by category, and number of spaces, and
|
|
tags, etc all at once.</p>
|
|
|
|
<p>If you're trying to manage GET queries manually it can get pretty hairy very fast.</p>
|
|
|
|
<p><a href="http://djangosnippets.org/snippets/2237/">This Django snippet</a> makes working with query strings in templates
|
|
a breeze.</p>
|
|
|
|
<h3 id="s23-satisfying-your-designer-with-typogrify"><a href="index.html#s23-satisfying-your-designer-with-typogrify">Satisfying Your Designer with Typogrify</a></h3>
|
|
|
|
<p>If you haven't heard of <a href="http://code.google.com/p/typogrify/">Typogrify</a> you should take a look at it. It makes it easy
|
|
to add all the typographic goodness your designers are looking for.</p>
|
|
|
|
<h2 id="s24-the-flat-page-trainwreck"><a href="index.html#s24-the-flat-page-trainwreck">The Flat Page Trainwreck</a></h2>
|
|
|
|
<p>Creating a site for a client is very different than creating a site for yourself.
|
|
For pretty much every client we've dealt with we've heard: "can't we just create
|
|
a new page at /drink-special/ for this special deal we're running?"</p>
|
|
|
|
<p>Having clients go through you to make new pages is simply too much overhead. We
|
|
needed a way to let clients create new pages (like <code>/drink-special/</code>) on the fly,
|
|
without our intervention.</p>
|
|
|
|
<p>Django has a "flatpages" app that solves this problem. Kind of.</p>
|
|
|
|
<p>When using flat pages clients need to do two things that are often too much for
|
|
non-technical people:</p>
|
|
|
|
<ul>
|
|
<li>Manage URLs manually.</li>
|
|
<li>Write all content as raw HTML in a single text field.</li>
|
|
</ul>
|
|
|
|
<p>We've tried a lot of Django CMS apps at Dumbwaiter, and none of them made us happy.
|
|
They all seemed to have some or all of the following problems:</p>
|
|
|
|
<ul>
|
|
<li>They take over your site and make you write a "Django-WhateverCMS site" instead of
|
|
a "Django site".</li>
|
|
<li>They're extremely feature-rich and complicated with features like
|
|
internationalization, redirects, versions, and many others. This is great if you
|
|
need the flexibility, but bad if your clients just need to create a couple of
|
|
pages.</li>
|
|
<li>They break <code>APPEND_TRAILING_SLASH</code> and make you clutter your <code>urls.py</code> files with
|
|
a bunch of extra code ot handle this.</li>
|
|
</ul>
|
|
|
|
<p>I finally got fed up and wrote my own Django CMS app: <a href="http://stoat.rtfd.org/">Stoat</a>. Stoat is designed
|
|
to be sleek, with only the features that our clients need.</p>
|
|
|
|
<p>It's not officially version 1.0 yet, but we're using it for a few clients and it's
|
|
working well. Check it out if you're looking for a more lightweight CMS app.</p>
|
|
|
|
<h2 id="s25-editing-with-vim"><a href="index.html#s25-editing-with-vim">Editing with Vim</a></h2>
|
|
|
|
<p>I <a href="https://stevelosh.com/blog/2010/09/coming-home-to-vim/">use Vim</a> to edit everything. Naturally I've found a bunch of plugins,
|
|
mappings and other tricks that make it even better when working on Django projects.</p>
|
|
|
|
<h3 id="s26-vim-for-django"><a href="index.html#s26-vim-for-django">Vim for Django</a></h3>
|
|
|
|
<p>There are a lot of ways to make Vim work with Django. I won't go into all of them in
|
|
this post, but a good place to start is <a href="https://code.djangoproject.com/wiki/UsingVimWithDjango">this Django wiki page</a>.</p>
|
|
|
|
<h3 id="s27-filetype-mappings"><a href="index.html#s27-filetype-mappings">Filetype Mappings</a></h3>
|
|
|
|
<p>Most files in a Django project have one of two extensions: <code>.py</code> and <code>.html</code>.
|
|
Unfortunately these extensions aren't unique to Django, so Vim doesn't automatically
|
|
set the correct <code>filetype</code> when you open one.</p>
|
|
|
|
<p>I've added a few mappings to my <code>.vimrc</code> to make it quick and easy to set the correct
|
|
<code>filetype</code>:</p>
|
|
|
|
<pre><code>nnoremap _dt :set ft=htmldjango<CR>
|
|
nnoremap _pd :set ft=python.django<CR></code></pre>
|
|
|
|
<p>I also have a few autocommands that set the filetype for me when I'm editing a file
|
|
whose name "sounds like" a Django file:</p>
|
|
|
|
<pre><code>au BufNewFile,BufRead admin.py setlocal filetype=python.django
|
|
au BufNewFile,BufRead urls.py setlocal filetype=python.django
|
|
au BufNewFile,BufRead models.py setlocal filetype=python.django
|
|
au BufNewFile,BufRead views.py setlocal filetype=python.django
|
|
au BufNewFile,BufRead settings.py setlocal filetype=python.django
|
|
au BufNewFile,BufRead forms.py setlocal filetype=python.django</code></pre>
|
|
|
|
<h3 id="s28-python-sanity-checking"><a href="index.html#s28-python-sanity-checking">Python Sanity Checking</a></h3>
|
|
|
|
<p>Lets be honest here: it takes a lot of work to turn Vim into an "IDE", and even then
|
|
it doesn't reach the level of something like Eclipse for Java. Anyone who claims it
|
|
has the same levels of integration and functionality is simply lying.</p>
|
|
|
|
<p>With that said I'll make an opinionated statement that is going to piss some of you
|
|
off.</p>
|
|
|
|
<p><strong>I am a programmer, not an IDE operator.</strong></p>
|
|
|
|
<p>I know Python.</p>
|
|
|
|
<p>I know Django.</p>
|
|
|
|
<p>I don't need to hit Cmd+Space twice for every line of code I write.</p>
|
|
|
|
<p>When someone asks me "how do you run your site" I do <strong>not</strong> answer: "click the green
|
|
triangle in Eclipse".</p>
|
|
|
|
<p>However, I am human. I do stupid things like forgetting a colon or forgetting an
|
|
import. To help me with those problems I've turned to <a href="http://www.vim.org/scripts/script.php?script_id=2736">Syntastic</a> and Kevin
|
|
Watters' <a href="https://github.com/kevinw/pyflakes">Pyflakes fork</a> for Vim.</p>
|
|
|
|
<p>Syntastic is a Vim plugin that adds on-the-fly syntax-checking for many different
|
|
file formats. If you have Pyflakes installed it will automatically show you errors
|
|
in your code.</p>
|
|
|
|
<p>Pyflakes doesn't have IDE-level integration with your code. It doesn't check that
|
|
whatever libraries you <code>import</code> actually exist. It simply checks that your files are
|
|
probably-valid Python, and tells you when they're not.</p>
|
|
|
|
<p>This is enough for me. It catches the stupid mistakes I make. The less-stupid,
|
|
more-subtle mistakes slip by it, but to be fair many of them would have slipped by an
|
|
"IDE" as well.</p>
|
|
|
|
<h3 id="s29-javascript-sanity-checking-and-folding"><a href="index.html#s29-javascript-sanity-checking-and-folding">Javascript Sanity Checking and Folding</a></h3>
|
|
|
|
<p>Syntastic also supports Javascript if you have Javascript Lint installed (<code>brew
|
|
install jsl</code> on OS X). It's not perfect but it <em>will</em> catch things like using
|
|
trailing commas in object literals.</p>
|
|
|
|
<p>Some people like using CTags to get an overview of their code. I take a more
|
|
low-tech approach and am in love with code folding. When I fold my code
|
|
I automatically get an overview of everything in each file.</p>
|
|
|
|
<p>By default Vim doesn't fold Javascript files, but you can add some basic, perfectly
|
|
serviceable folding with these two lines in your .vimrc:</p>
|
|
|
|
<pre><code>au FileType javascript setlocal foldmethod=marker
|
|
au FileType javascript setlocal foldmarker={,}
|
|
</code></pre>
|
|
|
|
<h3 id="s30-django-autocommands"><a href="index.html#s30-django-autocommands">Django Autocommands</a></h3>
|
|
|
|
<p>I <em>rarely</em> work with raw HTML files any more. Whenever I open a file ending in
|
|
<code>.html</code> it's almost always a Django template (or a <a href="http://jinja.pocoo.org/">Jinja</a> template, which has
|
|
a very similar syntax). I've added an autocommand to automatically set the correct
|
|
filetype whenever I open a <code>.html</code> file:</p>
|
|
|
|
<pre><code>au BufNewFile,BufRead *.html setlocal filetype=htmldjango</code></pre>
|
|
|
|
<p>I also have some autocommands that tweak how a few specific files are handled:</p>
|
|
|
|
<pre><code>au BufNewFile,BufRead urls.py setlocal nowrap
|
|
au BufNewFile,BufRead settings.py normal! zR
|
|
au BufNewFile,BufRead dashboard.py normal! zR</code></pre>
|
|
|
|
<p>This automatically unfolds <code>urls.py</code>, <code>dashboard.py</code> and <code>settings.py</code> (I prefer
|
|
seeing those unfolded) and unsets line wrapping for <code>urls.py</code> (lines in a <code>urls.py</code>
|
|
file can get long and are hard to read when wrapped).</p>
|
|
|
|
<h2 id="s31-conclusion"><a href="index.html#s31-conclusion">Conclusion</a></h2>
|
|
|
|
<p>I hope that this longer-than-expected blog entry has given you at least one or two
|
|
things to think about.</p>
|
|
|
|
<p>I've learned a lot while working with Django for Dumbwaiter every day, but I'm sure
|
|
there's still a lot I've missed. If you see something I could be doing better please
|
|
let me know!</p>
|
|
</article></main><hr class='main-separator' /><footer><nav><a href='https://github.com/sjl/'>GitHub</a> ・ <a href='https://twitter.com/stevelosh/'>Twitter</a> ・ <a href='https://instagram.com/thirtytwobirds/'>Instagram</a> ・ <a href='https://hg.stevelosh.com/.plan/'>.plan</a></nav></footer></body></html> |