Extending Django’s manage.py

I was sitting at work one day faced with a dilemma. This particular brain puzzle was translating a certain business need into code; something that, usually, isn’t too hard for me to do. This problem involved Etcetera’s equipment checkout module and the manner in which it interacts with the equipment inventory module.

The problem

When a patron submits a checkout request for equipment online, they specify the date the loan begins and the date the loan ends, usually in the ballpark of a week’s time. Our folks behind the scenes interacting with the system, say, when they’re speaking with a patron over the phone, need to see what equipment we have available and at what time. So when the equipment delivery guys attach a specific equipment item to a checkout, that equipment’s status within the system gets marked as reserved. The business need (now that you have the background story) is this: any equipment that’s reserved needs to show that it’s checked out during the time window that it’s actually checked out.

Equipment

Here’s such a piece of equipment.

My options

SQL queries

My first thought was to use what I had been taught in college: SQL update queries. Mmm! I’d devise a clever little script that would send a command-line update bit to psql every few minutes to switch over the equipment attached to checkouts that had a start date in the past and a return date in the future, and that weren’t already marked as completed/archived. I racked my poor feeble brain on this method for 6 hours before falling to my knees and begging to be rendered unconscious. I’m okay with SQL, but this was beyond my expertise and I shouldn’t have to waste this much time on something that seems fairly simple.

Using Django’s manage.py

Enter solution #2 (also known as why the task at hand already seemed simple). What I really wanted to do was use Django’s API, specifically its model and QuerySet APIs, to accomplish the database update. But this was supposed to be a temporal event: something that fired at certain points in time. I had already planned to run the SQL script using launchd (we’re hosting the Django app on a Mac OS X Server), but I had no idea how I’d get a Python script to run and properly find the Django module (I’m running my code inside a Python virtualenv — try it sometime). My missing piece in this puzzle was merely a shell script a better plist. So here’s what I’ve got.

Digging in

Creating the command class

I’ve got a management command built into my checkout app. Doing so requires that, within the app’s folder, you have another folder, management. Inside this folder, create a blank __init__.py file, and another folder, named commands. This folder must also contain a blank __init__.py file (remember, this blank file tells the Python interpreter that the folder contains runnable code; basically, it’s saying the folder is a module) as well as a Python script file that goes by the name of the command you want to make. My command is named equipment_status, so my file is equipment_status.py. Here’s my code for that file.

Note that the class name is Command and it’s a subclass of django.core.management.base.NoArgsCommand. That’s because my function doesn’t take any arguments. If yours will, subclass it off of …base.BaseCommand — and your code will be a little different than mine. Pretty much, mine uses the handle_noargs method, and acts like a regular Django/Python function, and uses the Django model API — a million-and-a-half times better than writing raw SQL. Trust me on that one.

Triggering the shell script plist with launchd

But now we need this to run every 120 seconds, and that’s where launchd and the /Library/LaunchAgents folder comes in. Here’s my plist file, named edu.missouristate.etc.equipment-status.plist, which is based on the reverse DNS hostname of the machine it’s made by, or run on, or something… it’s the custom, whatever. And it’s running on that website, so that’s the name.

These options for this plist file are ones I’ve found on Apple’s Manual Page for launchd. The page doesn’t do the best job in the world explaining what options you have to include and which you don’t (it’s a little daunting to look at), but I’ve made my way through it and developed the script above, which pretty much has the bare minimum.

  • StartInterval defines and integer of the number of seconds between each launch of the program/script. Here, I’ve set it to 120 seconds (2 minutes).
  • Label must be defined, and here it’s the filename without the plist extension. Best to be consistent.
  • WorkingDirectory is the cd command of the plist, if you will; it specifies the directory we’ll be working in, which in this case is the project folder within my virtualenv.
  • ProgramArguments is a string array that specifies the absolute path to the python binary I want to use (it’s in my virtualenv directory), the manage.py command, and the equipment_status method argument.
  • RunAtLoad is set only because I was trying to figure out if the script would run properly or not, and I didn’t want to have to wait 2 minutes after I loaded it into launchctl to see if it’d work.

So that’s it for the code. I had to make sure the plist had the appropriate permissions (it must be owned by root:wheel to run, otherwise you’ll get a ‘dubious ownership’ error when you attempt to load it — you may have to sudo chown it). There’s one more command you’ll have to run to enable this script.

sudo launchctl load -w /Library/LaunchAgents/edu.missouristate.etc.equipment-status.plist

After that it should run every 2 minutes. Check Console.app if you don’t think it’s working right. If you search for the name of the plist file, you’ll get a nice query of error messages (or stdout messages) if there are any. That’s it!

Also, props to Ted Kaemming for helping me figure some of the Django stuff out.

Edit: I’ve updated my post and the GitHub gist for the plist with Brooks Travis’s awesome tip: cutting out the shell script and specifying the virtualenv’s python as the one to run.