Lessons learned from my first Django project
Jul 01 2010
After learning Ruby and Rails for a few months, my first commercial gig involved building the server side components of an iPhone application using Python and Django. In the words of Morpheus, “Fate, it seems, is not without a sense of irony!” Luckily for me, a lot of what I had learned in Ruby land ported across to this project and my relative inexperience at this point helped – I didn’t have a lot of Rails-muscle-memory to unlearn!
For context, I was required to build a website that would be used for basic CRUD (Create, Read, Update, Delete) management of the assets that the iPhone application would present. I also had to build an API that the iPhone application would call to update its assets.
The setup
I used virtualenv to sandbox the app. virtualenv allows fine-grained control of the Python libraries, versions, etc. used in constructing the app – and the move to a production environment is then trivial. Also, it allows for different environments to coexist on your development machine without contaminating each other. I’ve noticed that rvm is providing Rubyists with a similar experience.
Admin pages
Django generates admin pages by default and if all you need is basic CRUD maintenance of your database, these do a terrific job. So as to jazz up the presentation, I used grapelli. The main problem I ran into was general confusion with the set up of templates. Here’s what we ended up doing: In settings.py set ADMIN_MEDIA_PREFIX='/site-media/' and make sure that the media folder of grappelli is copied over to a folder site-media in the project. Then start runserver by:
python manage.py runserver --adminmedia=`pwd`/site-media/
Tidy file paths
One of the more interesting problems with this project was that I needed to save uploaded media files in a directory corresponding to the id of the object to which they were associated. This would allow the local filesystem to be replicated on the iPhone and, knowing the structure of file paths would allow for efficient searching for media files. The issue here is that an object does not have an id until it has been saved.
Our first approach was to overload the save method by first calling the real save method, then redoing the file paths using the resulting id and then saving again. This was very messy and caused for duplicate objects to be created when importing from a CSV. However, this is a pattern that I found in a few places out there so here it is incase it is useful:
def save(self, *args, **kwargs):
# Overload the save method so that we get the id of the parent object
# move media files from tmp to category_thumbnails/category_id/
super(Tag, self).save(*args, **kwargs) # Call the "real" save() method.
file_name = os.path.basename(self.thumbnail.path)
tmp_path = settings.MEDIA_ROOT + 'tmp/'
# Transfer the contents of the tmp directory
dest_path = "category_thumbnails/%d/" % (self.id,)
# delete the dest_path if it exists
shutil.rmtree(settings.MEDIA_ROOT + dest_path,ignore_errors=True)
os.mkdir(settings.MEDIA_ROOT + dest_path)
shutil.move(tmp_path + file_name, settings.MEDIA_ROOT + dest_path + file_name)
self.thumbnail.name = dest_path + file_name
# Now update the database entry with the new filename
super(Tag, self).save(*args, **kwargs)
The solution that we ended up going with involved making a guid and then using something like this to set up the file path:
def category_thumbnail_path(instance, filename):
return 'category/%s/%s' % (instance.guid, filename)
thumbnail = models.FileField(upload_to=category_thumbnail_path)
Importing from a CSV
Import from CSV was frighteningly simple – the following two step process got me there:
- Line up the fields and use
get_or_createto either create the object or identify duplicates - Once the object has been created, use
addto add in associated objects that are in a many-to-many relationship with the object
# First read in a line from the file and set fields of your object
for import_file in files:
reader = csv.reader(open(import_file))
# skip header line
lines = list(reader)
for row in lines[1:]:
title = row[3]
sub_title = row[4]
# etc. ...
# Next use get_or_create to create the object
# Note that duplicates detected when both title and address match
try:
obj, created = Asset.objects.get_or_create(title=title, address=address,
defaults={'sub_title':sub_title,
'start_date': start_date,
'end_date':end_date,
'publish':publish
# etc. ...
})
# And here is how you add associated objects that are in many-to-many relationships with your object
print "Created=", created
if created:
if row[19] == '0':
for i in range(5):
obj.locations.add(Location.objects.get(pk=i+1))
else:
obj.locations.add(Location.objects.get(pk=int(row[19])))
API and django-piston
Need a RESTful API that responds in JSON or XML? Django-piston to the rescue! It took me the better part of a day of looking at some examples and reading the documentation to get to understand it but once I got the simplicity of the django-piston framework, I had an API in no time flat! I won’t elaborate on this any further – I found that the framework comes with excellent supporting code snippets and the interested reader should simply work through these.
Tricks for young players
Here’s one that I think you only learn from experience! The system generated timestamp associated to an object need not be unique nor represent the order in which the object was entered. Implement an index that increments on every save to the database to achieve this
class DBstate (models.Model):
# Counter that is incremented on every save to the database
# Used when calling the API to specify the state of the DB on the iPhone
save_count = models.IntegerField(default=0)
Of course you will need to overload each object’s save method to implement this.
def save(self, *args, **kwargs):
# set db_count
try:
counter = DBstate.objects.get(pk=1)
counter.save_count += 1
self.db_count = counter.save_count
counter.save()
except SaveCounter.DoesNotExist:
counter = DBstate()
counter.save()
self.db_count = counter.save_count
And this is definitely for fellow newbies – be very careful about the difference between using functions and return values when setting default values for fields. For instance,
guid = models.CharField(max_length=35, default = uuid.uuid1().hex, editable=False, blank=True)
will result in uuid.uuid1().hex being called once and its return value being used as the default value for the guid of all instances of the object. On the other hand, if you use the function only when specifying the default (note the missing parentheses):
guid = models.CharField(max_length=35, default = uuid.uuid1.hex, editable=False, blank=True)
then it will be called every time a default value is needed and each object will have a different guid.



