I'm actually a bit late on this one, but I've been so busy with my next Discovery Creative project that I just haven't had time for much else. Anyway, from June 15 through 22 AFI, in partnership with the Discovery Channel will be putting on their Silverdocs Documentary Film Festival. Check out the website for more details on the festival and the films they'll be showing, not to mention an example of how awesome my (and the rest of Discovery Creative, I guess) work is.
This is just a quick update. I'm phasing out the dydxtech.com. As a result, dydxtech.com now redirects to this page. None of the content from the original site is sticking around other than my portfolio, which is now available on this site at joshourisman.com/portfolio. I've done a half-assed job at re-skinning the portfolio page to look like the blog, and I'll put in some sort of link to it eventually.
Tags:
website
Comments: temporarily disabled!
I've now been living in Maryland and working at Discovery Creative for a whole month, so I think it's about time I started writing again! My first couple weeks I was here alone while Jessi finished things up in Boston I had little motivation to do much outside of work, so I found myself going in to work early and home (I was staying with my aunt and uncle who graciously offered me a place to stay at their home in Bethesda during the limbo time between moving out of the condo in Somerville and into the apartment in Silver Spring) late. That left me pretty exhausted at the end of the day so relaxing, eating, and sleeping were much higher on my to do list than writing. Since Jessi got down here my time outside work has been dedicated to unpacking boxes and transmitting what little I know of the area to her and her sister Becky who flew out to help with the move. Now, however, we're unpacked—if not completely then at least enough to be comfortable—with most of our furniture in place and awaiting delivery on the few remaining items that we've purchased. So time to start looking towards the future again.
My experience here thus far suggests that there's a lot of interesting work ahead. I've got a few Django projects already going on, including working with some new stuff like OpenID, OpenCalais, Clickpass, Twitter, and many other things. I'll probably be starting work on a project using Google App Engine in the near future, and I've already begun learning about and starting to work on iPhone apps as well. So I should have lots of good fodder for technical posts in the coming months!
On top of that there's a whole new city to explore, and nearly the entirety of my family spend time with. Before we knew we were going to be moving, Jessi and I had been planning on getting kayaks this summer so we could spend some time on the Charles and Mystic rivers, maybe the Harbor Islands, and hopefully take them up to the Adirondacks to explore the lakes up there. That plan certainly hasn't changed as now we've got the Potomac and other bodies of water to play with. Plus we're now so close to Shenandoah that it would be a crime not to get in some camping and backpacking. (And after the missed opportunity last winter, I'm definitely planning on some winter backpacking in the Senandoah back country this year!)
So with any luck I should be doing a lot of writing on a lot of different topics in the future. At the very least I need to do some work on this site as I want to integrate my portfolio into the personal site and phase out the business one. And working on this site always seems to give my something to write about.
Tags:
backpacking,
boston,
camping,
dc,
discovery,
furniture,
kayak,
maryland,
massachusetts,
move,
outdoors,
washington,
work
Comments: temporarily disabled!
If you follow me on Twitter or Facebook you'll likely have seen my big news: I have a new job! That's right, I've been hired on as a web developer for Discovery Communications (the parent company of the Discovery Channel). More specifically, I'll be working at Discovery Creative, the subset of Discovery Networks that works on things like web sites (duh) and advertising. It's a pretty awesome job, and I can't wait to get started.
But, true to form, the transition to this next stage in my life is not as simple as that. The job is not here in Boston, rather it's down in Silver Spring, Maryland (just outside the northern tip of DC). So just three short years after moving to Boston, we're moving to Maryland! This is a little less crazy than it might sound. Nearly all of my family is down there, within about 10 miles of Silver Spring. The next largest concentration of my family members (which consists of just my grandmother and an aunt) is in South Jersey, about a 3 hours drive away. Other than that, there's no single place in the coutry where I could live near more than a single blood relation of mine (although there are a number of other places we could go to be overrun with members of Jessi's much larger family). On top of that, Jessi's sister and her daughter (our niece) are moving to Virginia this summer, about an hour away, and my sister is going to be in DC for at least the summer after her graduation in May. Strangely enough, DC actually seems to make a lot more sense for us logistically. The only real complication is what to do with our condo. Fortunately it's about four blocks away from Harvard Law School, basically on the campus of Lesley University, and within walking distance of Tufts University. So we're pretty confident that we'll be able to get it rented pretty quickly, and after some consulting on our mortgage (Best thing about the current economy? We can refinance and knock a good 2% off our interest rate!) it seems as though it should work out pretty well financially too.
So yet again this blog will turn into the chronical of a move. A much shorter move than last time (thankfully; as much as I enjoy driving cross-country, I really am not in the mood to do it again right now), but one that will, I think, prove quite interesting.
Oh, and the best thing about this new job? They have a dinosaur in the lobby!

How cool is that?
[Edit: Corrected a mistake. The parent company of the Discovery Channel is Discovery Communications, not Discovery Networks! Thanks Matt!]

Amazon has released a Kindle for iPhone (iTunes store link) app. It's exactly what it sounds like, and my first impression is that it may well be the straw that breaks the camel's back and pushes me to adopt the Kindle.
I woke up this morning to discover that Amazon had released their Kindle for iPhone app. We all knew this was coming, though I don't think anyone (at least, not me) suspected it would come so soon. I have been anxiously awaiting a good eBook solution, and thus far the Kindle has seemed like the best bet, but I haven't yet been totally convinced. Fortunately the Kindle for iPhone app is free, removing the first barrier for entry to buying into Amazon's system. On top of that they allow you to download the first chapter of books in the Kindle store for free to preview them. This provides the perfect opportunity to test it out, so that's exactly what I did.
Immediately upon downloading the app my first task was to get some content to test it out with. The home screen provides a promising 'Get Books' button, but unfortunately that merely takes you to a screen entitled 'How to Get Books' which does nothing other than tell you go 'Get the best shopping experience by visiting www.amazon.com/kindlestore on your Mac or PC'. It also, helpfully, tells you that you can 'use Safari on your iPhone to buy books' (and, in one small nod to decent functionality provides you with a link that will open Safari to the Kindle store on your iPhone). So my first experience with the app was one of disappointment; I wasn't near my computer, and Amazon.com isn't the most mobile Safari-friendly site in the world. So my next thought was to check the Amazon.com iPhone app (iTunes store link). This app allows you to browse Amazon's vast catalogue of products and even purchase them. Unfortunately, for the Kindle editions of books it only allows you to add them to your wish list and not purchase them directly. I'm generously assuming that the next version of the Kindle for iPhone app, or the Amazon.com iPhone app will remedy this situation and provide an integrated way to purchase Kindle content. In the meantime, I used mobile Safari to grab a preview of the Kindle edition of one of the books on my wish list. I chose Sam Harris' The End of Faith, as I've been wanting to read it for a while.

I haven't used an actual Kindle yet, so I don't know how it compares, but the Kindle for iPhone app provides a pretty good interface for browsing your library. You can easily sort them by recentness, title, or author, and there's an 'Archived Items' section that allows you to see and download books that you've purchased but which haven't yet been added to your iPhone (when you purchase a book from the Kindle store it gives you a choice of which device(s) it should be pushed to initially). It even helpfully provides you with an indicator of whether you have the full book or just a sample, although it does not appear to tell you if you've started reading a book yet or how far through it you are which would be a nice touch.
Upon selecting my book sample to read, I was initially taken to the first page of text of my single chapter. However the book's title page, copyright page, and a hyperlinked table of contents (among other things) were also present. Pages are turned, intuitively, with a swipe of the finger to the right or to the left. A single tap on the screen reveals various options such as a back button in case you've followed any links (such as on the table of contents, or in the case of endnotes), a button to add a book mark, a button to adjust the font size (I chose to go with the smallest size in order to minimize the number of page turns necessary), a reload button, and a less than clearly labeled button that looks like an open book. It also provides you with a slider that allows you to easily scroll through the book, and an indicator of how far through the book your indexed, I believe, by word number (the screen I'm currently looking at indicates that my location is 387-395). The less-than-obvious button is actually a 'Go to' button which gives you a number of options including 'Cover', 'Table of Contents', 'Beginning', 'Location' (which allows you to enter the number of a particular location), and any notes or marks you may have added. A pretty good system, though it was only through experimentation that I figured out what it actually was.

The reading experience, I found to be surprisingly good. I've played around with the Stanza eBook app for iPhone, but was never really able to stick with it for long periods of time. I don't know if it was the choice of fonts, the quality of type-setting, or what, but I found reading with the Kindle for iPhone app to be great. One of the common criticisms I've heard of the Kindle and of other eBook readers is that it doesn't simply 'disappear' the way a paper book does when you're reading it. Maybe it's the intuitive touchscreen interface the iPhone provides, but I definitely didn't find this to be the case. In fact I blazed through my sample first chapter in no time at all and rarely noticed that I was reading off an iPhone. The text is exceptionally clear and easy to read, and while I'd prefer the larger screen of an actual reader device, I found there to be plenty of text on the screen that I wasn't constantly flipping to the next page. In fact I was probably only about halfway through the sample first chapter before I decided that spending the $7.96 to buy the full eBook is totally worth it. Basically, I'm a convert.
I still have concerns about the Kindle in general—I don't like the locked-in nature of using Amazon's DRMed content, and the price is currently just a little more that I'm willing to pay for a reader (especially now that I can read the books on my iPhone)—but I'm now convinced that my initial feelings, that eBooks are the way of the future and it won't be long before I, at least, am doing most of my reading electronically, were right on the mark. I'll definitely be buying a book or two through the Kindle store now, though I'm still hesitant to invest in much of a Kindle library in case I end up deciding to go with some other solution, but if Amazon comes out with a Kindle 3 that is a little more in line with what I want from a dedicated reader, I'll almost definitely be purchasing it. And I'm still not ruling out the purchase of a Kindle 2 entirely. If Amazon were to drop the price by $100, I'd buy one today. But I do very much hope that Amazon and the publishing companies will find some way forward for more open content. The fear of getting trapped into a system that ends up being inferior will certainly continue to curb my investment in the Kindle, but since Amazon has stated, and now demonstrated, that Kindle content will be made available on other devices, they may well have sold me on their eBook ecosystem.
I can't believe I haven't written about this yet, but BostonChefs.com's the unofficial guide to Boston's Restaurant Week for Winter 2009 is live. I've been working on this particular site since 2007 when I first help make some PHP-based improvements to it. Last summer it became my first professional Django-based project when I completely redeveloped it in Django. This year I've made some further improvements to the Django codebase, including a completely re-implemented and much improved Google Maps mashup that lets you view the restaurants geographically.
Previously this had been using some old code that I had inherited from the original site. It was pretty good code, but more modern tools allowed me to vastly simplify it. Specifically, instead of always loading every restaurant and populating the map with markers, then simply zooming in on a specific neighborhood or restaurant, it now only loads the restaurants appropriate for the current target. If you select a specific neighborhood, it only loads the restaurants for that neighborhood, and puts markers on the map for them. Because of that I was able to use Google's API to automatically set the bounds and zoom level of the map to best show those restaurants. This removes the need to manually pick a set of coordinates and zoom level for each neighborhood (having, then, to fine tune it for each neighborhood individually) and instead takes care of all that automatically. The result is a map that's simpler to manage, presents the relevant information more intelligently, and is much quicker to load. All in all, I think it's a pretty big improvement.
I just finished up with some relatively simple, but still fun modifications to the Django admin site for one of the projects I'm working on. For this project I needed to create a many-to-many relationship between two models with ordering information associated with it. This is fairly easy to accomplish by creating a many-to-many with an intermediary table. But providing a convenient mechanism for managing that information is a little trickier. By default you'll just end up with a text entry box in which to manually type the order for that item. This gets pretty old pretty fast, so finding a better method is important. In the past I've used jQuery to add drag and drop re-ordering to inline edited models, but this time I needed to do a little more as well. Specifically I wanted to make it easier to add more inline item. Ordinarily you just set an arbitrary number of empty boxes to have displayed (by default 3) and if you want more you have to fill those boxes, then hit 'Save and continue editing' to get three more. This is a pretty crappy way to do it (but the only way without introducing unwanted dependencies).
Some googling revealed that Arne Brodowski had done pretty much exactly what I wanted to so, so I worked up some modified version of his scripts. The first step was setting it up to hide entries that were marked for deletion. Arne provided a prototype script to do this, but I made a few modifications that clean it up a bit and made it actually work (at least with jQuery 1.3.1). What I ended up with was this somewhat more elegant looking script:
jQuery(function($) {
$("div.inline-related input:checkbox[id$=DELETE]").change(function() {
if ($(this).attr('checked')) {
$(this).parents('div.inline-related').children('fieldset.module').addClass('collapsed');
} else {
$(this).parents('div.inline-related').children('fieldset.module').removeClass('collapsed');
}
});
});
The changes I made are mainly in the selector for grabbing the checkboxes ($("div.inline-related input:checkbox[id$=DELETE]"), and in the method for checking whether or not the box is actually checked ($(this).attr('checked')). With those changes it works exactly as advertised, which is a pretty handy bit of functionality.
The next step was slightly more complex. To be able to dynamically add more relationships without having to 'Save and continue editing', I made basically the same template changes as Arne did, and fortunately this time didn't need to make many changes to the script. I basically just removed the 'return false' from the end and wound up with this:
function increment_form_ids(el, to, name) {
var from = to-1 ;
$(':input', $(el)).each(function(i,e){
var old_name = $(e).attr('name');
var old_id = $(e).attr('id');
$(e).attr('name', old_name.replace(from, to));
$(e).attr('id', old_id.replace(from, to));
$(e).val('');
});
}
function add_inline_form(name) {
var first = $('#id_'+name+'-0-id').parents('.inline-related');
var last = $(first).parent().children('.last-related');
var copy = $(last).clone(true);
var count = $(first).parent().children('.inline-related').length;
$(last).removeClass('last-related');
$(last).after(copy);
$('input#id_'+name+'-TOTAL_FORMS').val(count+1);
increment_form_ids($(first).parents('.inline-group').children('.last-related'), count, name);
$(first).parents('.inline-group').children('.last-related').find('input[id$=order]').val(0);
$('div.inline-group').find('div.inline-related').each(function(i) {
$(this).find('input[id$=order]').val(i+1);
});
}
You'll probably notice that I did actually make one other change. After I had gotten all this working, I realized there was one problem: If when starting from scratch you just happened to enter your choices in the order you wanted it wouldn't actually save that ordering information. By default (with my model definitions anyway) everything was assigned an order of 0 until you actually dragged things around to reorder them. This might be ok for some applications, but just won't work for this particular project. So I added those three lines of code to the function for adding new entries and also made sure that it's also done when you first load the page. This way you can be sure that everything will always be numbered appropriately and you won't end up with any unwanted 0s in your ordering information.
I've written in the past about the dismal state of reading in America, and how depressing I, as an avid reader, find it. Today, however, I was pleased to discover via mssv.net, that recent NEA study shows that reading is on the rise again, especially among 18-24 year olds.
With any luck, this will result in an increase in demand for eBooks and eBook technology, so that we might finally start to see some of the advancements I'm looking for (such as my ultimate eBook reader and my eBook store idea). eBook technology does seem to be pretty steadily advancing towards what I'd like to see, and I think it may be only a matter of months before I finally see a device that convinces me to take the plunge (maybe even the Kindle 2, if it ever comes out).
I recently went live with another Django project. The Small Design Firm website uses Django and Prototype frameworks to showcase the company's previous projects and news articles. This was a pretty straightforward project and didn't really involve any real hacking, but was none the less a good reminder of how pleasant it is to work in Django and how quickly one can develop a website with it.
This post is actually a few weeks overdue, but we were away on vacation for a couple weeks, and I'm still in the process of catching up from everything. Anyway, the new BostonChefs.com is live! There haven't been any major changes since the public beta I announced a few months ago, but there have been a few minor tweaks and even a couple new features added. This is my first major Django project to go live, and while there's still some work to be done it's nice to see the product of so much work finally going out the door.
But there's no rest for the weary, and I've got a fairly large number of more projects in the pipeline including at least three more Django projects of varying sizes, and one in CodeIgniter. The next few months, if not the whole of 2009, promise to be quite busy!
As you probably know, I've been working on a Django-based re-build of BostonChefs.com (the new version of which is actually live now, but due to DNS propagation issues isn't yet available to 100% of people which is why I haven't yet written a post about it). Among other things, BostonChefs.com provides information on some of the fantastic restaurants in the Boston area. One piece of information it provides is the hours of operation of those restaurants. In order to store this information I created a model called HoursOfOperation. It looks like this:
class HoursOfOperation(models.Model):
DAY_CHOICES = (
('0', 'Sun'),
('1', 'Mon'),
('2', 'Tue'),
('3', 'Wed'),
('4', 'Thur'),
('5', 'Fri'),
('6', 'Sat'),
)
restaurant = models.ForeignKey("Restaurant")
meal_period = models.ForeignKey("MealPeriod")
day = models.CharField(max_length=3, choices=DAY_CHOICES)
open_time = models.TimeField(default=datetime.datetime.now)
close_time = models.TimeField(default=datetime.datetime.now)
def _get_hours(self):
return "%s - %s" % (self.open_time.strftime('%I:%M%p'), self.close_time.strftime('%I:%M %p'))
hours = property(_get_hours)
As you can see, each 'hour' is related to a restaurant and a meal period, which allows us to display the information in a manner similar to that you might find on a store's front sign. For example, if you go to the Grill 23 & Bar page (my personal favorite restaurant in Boston, although Craigie on Main is a decent challenger), you'll see something like this:
DINNER
* Sun: 5:30 p.m.-10 p.m.
* Mon-Thur: 5:30 p.m.-10:30 p.m.
* Fri: 5:30 p.m.-11 p.m.
* Sat: 5 p.m.-11 p.m.
Building a list like that out of the above model proved slightly more difficult that I might have hoped. It required quite a lot of template logic, including writing a custom filter. The block of template code necessary to generate that list looks like this:
<div class="hours">
{% regroup restaurant.hoursofoperation_set.all by meal_period as periods %}
{% for period in periods %}
<div class="hoursMealPeriod">{{ period.grouper }}</div>
{% regroup period.list by hours as hour_list %}
<ul>
{% for hour in hour_list %}
<li>{{ hour.list|collapsedays }}</li>
{% endfor %}
</ul>
{% endfor %}
</div>
As you can see, somewhat complex. Those nested {% regroup %}s can be nasty to wrap your head around, if nothing else. But basically it's taking the set of HoursOfOperation objects related to the restaurant, grouping them by meal period, then taking the subset of those objects for each meal period, and grouping those by the hours of the day they represent. So what you're then left with is a list of all the different time periods (still represented as HoursOfOperation objects) that the restaurant is open for a given meal period, and the days on which it is open during those hours. As you can see above, the days are represented by number of the day of the week (0 for Sunday through 6 for Saturday).
Converting that list integers into something like 'Mon, Wed-Fri' was not very easy, and certainly not something I wanted to try to tackle using Django's template tags. I ended up drawing heavily on my hazy memories of CS 127 (many thanks to Dave who taught me all about recursion way back then) and creating a filter that considers the list of HoursOfOperation objects as a list of those integers, then recursively converts it into a list of lists representing the subsets of contiguous days in the list. So if you start out with [1, 3, 4, 5] you end up with [[1, 1], [3, 5]] which is then converted into 'Mon, Wed-Fri'. After several false starts I ended up with this beauty of a Django template filter:
from django.template import Library
from django.template.defaultfilters import time
from types import ListType
register = Library()
def simplify(index, found, days):
high = index+1
mid = index
low = index-1
if not found:
days[low] = [days[low], days[low]]
if high >= len(days):
if not isinstance(days[-1], ListType):
if days[-1] == days[-2][1]:
days.pop(-1)
else:
days[-1] = [days[-1], days[-1]]
return days
if int(days[high].day) - int(days[mid].day) == 1 and (found or int(days[mid].day) - int(days[low][0].day) == 1):
days[low][1] = days[high]
days.pop(mid)
high = high-1
found = True
else:
if found:
days.pop(mid)
found = False
return simplify(high, found, days)
@register.filter
def collapsedays(value):
hours = "%s-%s" % (time(value[0].open_time), time(value[0].close_time))
days = simplify(1, False, value)
for i in range(len(days)):
if days[i][0] == days[i][1]:
days[i] = days[i][0].get_day_display()
else:
days[i] = "%s-%s" % (days[i][0].get_day_display(),
days[i][1].get_day_display())
return "%s: %s" % (', '.join(days), hours)
Tags:
boston,
bostonchefs,
code,
django,
filter,
food,
massachusetts,
programming,
python,
restaurants,
template
Comments: temporarily disabled!
Really, this was a pretty major oversight on my part, but I just now finished added a 'status' field to each entry in my blog app. It's a pretty simple thing in and of itself, I just added a tiny bit of code to my Entry model:
STATUS_CHOICES = (
('dr', 'draft'),
('ac', 'active'),
('ar', 'archive'),
)
status = models.CharField(choices=STATUS_CHOICES, max_length=2, default='dr')
And then a custom manager as well:
class EntryManager(models.Manager):
def active(self):
return self.get_query_set().filter(status='ac')
Remembering, of course, to make sure that manager was actually accessible from the model (in this case by adding 'objects = EntryManager()' to my model class).
Once I did that I was able to change the Entry.objects.all() in my view(s) to Entry.objects.active() and now I'm able to write a post and save it without actually publishing! As I said, something of a bonehead move that I didn't do this in the first place.
Since I was messing around in my code anyway I decided to clean up the admin for my Entry model as well. Since I'm the only one who ever uses the admin for my blog, I hadn't bothered to before, but now I've got a little more info available to me when looking at my entries:
class EntryAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('title',)}
save_on_top = True
list_display = ['title', 'post_date', 'site_list', 'status',]
list_filter = ('sites', 'status',)
So my admin now looks something like this:

A cool thing there is the 'site_list' column. By default, you can't use a ManyToManyField in the list_display for your model. But I've dealt with this before, and it's only 5 lines of code to make this work:
def site_list(self):
if self.sites:
sites = [site.name for site in self.sites.all()]
string = ", ".join(sites)
return string
Good stuff!
Later this month Jessi and I are going on a 2,500+ mile road trip. We'll be spending Christmas in Florida with some of Jessi's family and new years in Georgia with some friends of ours. Why on Earth would we spend so much time on the road (the long legs are approximately 17 hours of driving each) when we could just hop a jet and be there in a couple hours? Well, it's saving us a ton of money, and since it's a longer trip the amount of time driving really isn't that significant.
How much money is this saving us, you might ask? Well, when I was initially planning this out I just used Google maps to figure out the distance, divided by MPG our car gets (mid 30s on the highway, it's an '06 Mazda 3), then multiplied by what I figured would be a realistic price of gas in December. At the time (this was a few months ago) I figured that $5/gal would be reasonable, and the cost came out to around $350. It would cost that much just for us to fly one way from Boston to Florida, not to mention a multi-city itinerary and the additional costs of having to get from the airport to wherever we were going.
Now, however, I've discovered an awesome new website: CostToDrive.com. It basically takes care of all that for you, and with up to date information. So I just entered our starting and ending point, and what kind of car we're driving (currently it doesn't handle multi-destination trips, but I've apparently still retained some basic arithmetic skills). Not only does it you an estimate of how much the trip will cost, but it actually gives you driving directions that include the specific gas stations you should go to in order to get the best price. How awesome is that?
Apparently, at current prices, it will cost us $59.01 to drive from Somerville to Amelia Island at an average cost per gallon of $1.73, $20.79 to drive from Amelia Island to Athens Georgia at $1.82/gal., and $56.80 to drive from Athens back to Somerville at $1.54/gal. (Seriously? When I first got my car in '99 it cost me $1.75/gal. to fill up in Oakland, CA!) So all in all the trip should cost right around $140 (assuming current gas prices, but who knows what will really happen there). Not bad! Definitely not worth my money to fly.
Every so often you run into a situation where Django's built-in form fields and widgets just don't meet your needs. I ran into this the other day when creating a credit card processing form. I wanted an easy way for the user to enter the expiration month and date of their card, but the tools provided by django only gave me a single text field and the option to use a javascript date picker. Neither of those was quite what I wanted, I just wanted two selects that would allow the user to pick the month and the year, as on pretty much every credit card form you've ever filled out online.
The obvious option would be to make two separate fields on your model, one for month, and one for year. But I don't really like that option. I would much rather have both selects show up on the same line, which is not the behavior you'd get with two separate fields. So I decided to write a custom widget and field to accomplish this. It was actually surprisingly easy to do. Probably the most difficult part was coming up with a decent way to populate the selects with worthwhile options. I'm not entirely pleased with the route I took there, but it's easy enough to change later. Here's my code:
from django import forms
import datetime
class MonthYearWidget(forms.MultiWidget):
"""
A widget that splits a date into Month/Year with selects.
"""
def __init__(self, attrs=None):
months = (
('01', 'Jan (01)'),
('02', 'Feb (02)'),
('03', 'Mar (03)'),
('04', 'Apr (04)'),
('05', 'May (05)'),
('06', 'Jun (06)'),
('07', 'Jul (07)'),
('08', 'Aug (08)'),
('09', 'Sep (09)'),
('10', 'Oct (10)'),
('11', 'Nov (11)'),
('12', 'Dec (12)'),
)
year = int(datetime.date.today().year)
year_digits = range(year, year+10)
years = [(year, year) for year in year_digits]
widgets = (forms.Select(attrs=attrs, choices=months), forms.Select(attrs=attrs, choices=years))
super(MonthYearWidget, self).__init__(widgets, attrs)
def decompress(self, value):
if value:
return [value.month, value.year]
return [None, None]
def render(self, name, value, attrs=None):
try:
value = datetime.date(month=int(value[0]), year=int(value[1]), day=1)
except:
value = ''
return super(MonthYearWidget, self).render(name, value, attrs)
class MonthYearField(forms.MultiValueField):
def compress(self, data_list):
if data_list:
return datetime.date(year=int(data_list[1]), month=int(data_list[0]), day=1)
return datetime.date.today()
Simple, no? The results end up looking like so:

Django provides an extraordinarily easy way of integrating file uploads into your site in the FileField (also the ImageField which provides some nifty image-specific functionality). However as soon as you start dealing with file uploads you have to worry about where those files are going to be stored. To take care of this, the 'upload_to' argument is set when declaring your FileField which is a local filesystem path that will be appended to your MEDIA_ROOT to the upload location (you could also use an absolute path). By default this is just a static string, though it does handle strftime arguments to allow some flexibility and will look something like this:
def model(models.Model):
file = models.FileField(upload_to='my/file/uploads/')
...
However that's not always going to cut it. Sometimes you're going to want more flexibility that even just separating the files out by date information. In that case you can take advantage of the fact that a callable can be passed as an argument. In one of my projects I've defined a function that returns the appropriate path depending on a couple of different variables. In this particular case, I know that all uploads for a certain model will be PDFs, while all uploads for certain other models will be images, so I'm able to pretty easily generate appropriate paths for those uploads. We also wanted to obfuscate the filenames so that someone couldn't easily guess the path for other files, and didn't really like the way that Django simply keeps appending '_'s to filenames in the event of a filename collision until it ends up with a unique name. To handle that I simply generate a random string to use as the filename, which fits our needs quite nicely. In my specific case, the function lives in myproject.lib.files and looks like this:
import random, string
from django.contrib.contenttypes.models import ContentType
def get_path(instance, filename):
ctype = ContentType.objects.get_for_model(instance)
model = ctype.model
app = ctype.app_label
extension = filename.split('.')[-1]
dir = "site"
if model == "job":
dir += "/pdf/job_attachment"
else:
dir += "/img/%s" % app
if model == "image_type_1":
dir += "/type1/%s" % instance.category
elif model == "image_type_2":
dir += "/type2"
elif model == "restaurant":
dir += "/logo"
else:
dir += "/%s" % model
chars = string.letters + string.digits
name = string.join(random.sample(chars, 8), '')
return "%s/%s.%s" % (dir, name, extension)
As you can see, it's pretty simple to generate an upload path based on pretty much any characteristics you want. To then tell Django to use this function to get the upload path rather than a static string you modify your model definition like so:
from myproject.lib.files import get_path
def model(models.Model):
file = models.FileField(upload_to=get_path)
...
And that's all it takes. Now all my uploads are automatically renamed and uploaded into the particular directory structure that we want. And since all the logic involved is in a single function it's extremely easy to change it at any point if we decide to alter our directory structure.
More posts >