Dev360.com - Web Development and Beyond

In a split-second, customers will determine if your site is worthy of their time. Can you afford to lose them?

Blog

Categories

Archive for the ‘Django’ Category

Using a text input for ManyToMany relations in Django

Sunday, February 7th, 2010

Problem:

You have a many-to-many relationship on a model — i.e. Video has many Tags — and you want to be able to use just a text input to manage that relationship.

I recently came across this problem while working on a Video site that called for tagging of uploaded Media. Django’s default way of dealing with ManyToMany fields (checkboxes) did not seem appropriate since the UX would degrade very quickly as the number of tags grow. The UI paradigm that everyone is familiar with is obviously to enter the tags in a comma-delimited format, like how most popular blog sites have it. This post will walk you through how to implement this in a ModelForm that can be used in your admin. This tutorial is for Django version 1.1.1. In retrospect, I guess a custom widget may have been the option with best reusability, but if for nothing else, this post could give you a pretty good idea of how flexible Django is to extend and modify.

Models

First off, we create the VideoTag and Video class. Notice how the tags attribute of the Video model has editable=False - since we do not want to bother with the select boxes in our form.

from django.utils.translation import ugettext as _
from django.db import models
from django.template.defaultfilters import slugify

# models.py
class VideoTag(models.Model):
	slug = models.SlugField(max_length=30, editable=False, blank=True, unique=True)
	name = models.CharField(_('name'), max_length=30)
	parent = models.ForeignKey('VideoTag', verbose_name=_('parent'), related_name='children', blank=True, null=True)

	def __str__(self):
		return '%s' % (self.name)

	def __unicode__(self):
		return u'%s' % (self.name)

	def save(self, force_insert=False, force_update=False):
		self.slug = slugify(self.name)
		super(VideoTag, self).save(force_insert, force_update)

class Video(models.Model):
	title = models.CharField(_('title'), max_length=32)
	summary = models.TextField(_('summary'), max_length=300)
	tags = models.ManyToManyField(VideoTag, verbose_name=_('tags'), related_name='videos', blank=True, editable=False)

Forms

Now that we have the models, we can run go straight to the forms part, where all the relevant code is. As you can see, there are two steps to this: the first part is to override the initialization of the form to set the initial value of the tags_text field to read the tags M2M fields; the second part is to capture the input and save it as related objects.

#forms.py
from django.utils.translation import ugettext as _
from django import forms
from django.template.defaultfilters import slugify

from your_app.models import Video, VideoTag

class VideoForm(forms.ModelForm):
	tags_text = forms.CharField(label=_("video tags"), required=False, help_text=_("A comma-delimited list of the tags that you would like to tag this with"),)

	def __init__(self, *args, **kwargs):
		super(VideoForm, self).__init__(*args, **kwargs)

		# args.__len__ is greater than zero if it is a postback
		if len(args) == 0 and self.instance != None and self.instance.id != None:
			tags = list(self.instance.tags.all().order_by('name'))
			if len(tags) > 0:
				initial_text = reduce(lambda x, y: u'%s, %s' % (x, y.name), tags)
				self.fields["tags_text"].initial = initial_text

	def save(self, commit=True):
		instance = super(VideoForm, self).save(commit=True)
		tags = self.cleaned_data["tags_text"]
		instance.tags.clear()
		if tags:
			for tag in tags.split(','):
				if tag.lower().strip() != "":
					video_tag = None
					tag = tag.strip()
					try:
						video_tag = VideoTag.objects.get(slug=slugify(tag))
					except VideoTag.DoesNotExist:
						video_tag = VideoTag()
						video_tag.name = tag
						video_tag.save()
					instance.tags.add(video_tag)
		return instance

	def save_m2m(self):
		pass

	class Meta:
		model = Video

Admin

Next, you may want to use this form in your admin, so let’s set the admin to use the correct form:

#admin.py
from django.contrib import admin
from your_app.models import Video
from your_app.forms import VideoForm

class VideoAdmin(admin.ModelAdmin):
	form = VideoForm
admin.site.register(Video, VideoAdmin)

ManyToManyField and Ordered fields in Django ModelForms

Monday, June 15th, 2009

I was working on some code and realized that ManyToManyFields always show up last in a ModelForm, regardless if you specify the order in the Meta fields property. Apparently this is due to be fixed in Django 1.1 which is right around the corner.. unfortunately I’m on 1.0 and feel anxious about moving 10+ sites to 1.1 beta right at this moment. I also felt it was a bit inappropriate to diff my Django source.

Please be aware that this is a hack. It is not an optimal solution. Stuff like form.as_* will NOT work with the solution below. All this does is add a property called ordered_fields to your ModelForm which implements the correct behavior.

# models.py
class Bar(models.Model):
	pass

class Foo(models.Model):
        # ordinarily, Django 1.0 would list the regular_field first and all M2Ms last
	many_to_many = models.ManyToManyField('Bar', related_name='foos')
	regular_field = models.CharField(max_length=10)
# forms.py
class FooForm(forms.ModelForm):
	def ordered_fields(self):
		list = []
		for field_name in self._meta.fields:
			list.append(self[field_name])
		return list

	class Meta:
		model = Foo
		fields = ('many_to_many',
			     'regular_field',)
#Template:

<form action="{{ app_path }}" method="post" id="foo-form">
	<fieldset class="module aligned ()">
	{% for field in form.ordered_fields %}
		<div class="form-row thin wide ()">

			{{ field.label_tag }}
			{{ field }}
			{{ field.errors }}
			{% if field.help_text %}
			<p class="help">{{ field.help_text }}</p>
			{% endif %}
		</div>
	{% endfor %}
	</fieldset>
	<div class="submit-row aligned ()">
		<input class="default" type="submit" value="Send" />
	</div>
</form>

ImageField not validating in Django ModelForm

Friday, June 5th, 2009

The other day I was working on a Django ModelForm that had an ImageField in it and to my great surprise the ImageField would not validate in the ModelForm, even when I had selected an image. The solution was simple enough: add request.FILES to the instantiation parameters of the ModelForm.. DUH!

    #ProfileForm is a ModelForm
    form = ProfileForm(request.POST, request.FILES, instance=profile)

Rendering a RSS feed in Django

Sunday, May 24th, 2009

I have been working to get the new version of DEV360.com out the door and found myself in need of displaying RSS items from my Wordpress-powered blog on the DEV360 site which is developed in Django.

There were two libraries that I found for parsing RSS; RSS.py by Mark Nottingham which unfortunately didn’t support RSS 2.0, and Mark Pilgrim’s feedparser.py. I settled on the latter since it contains more features, has easier syntax and has better unit-test coverage.

For what I’m doing, a template tag works out best for rendering the RSS items; let’s fast-forward to the code:

foo.html:

{% load rss_feed %}
{% rss_feed %}

[application]/templatetags.py:

import feedparser
from datetime import datetime
from django.template import Library, Node
register = Library()

@register.simple_tag
def rss_feed(itemCount=5):
	channel = feedparser.parse('http://blog.dev360.com/feed/')

	html = []
	html.append('<div id="rss-feed">')
	html.append('	<h1>Latest blog posts</h1>')
	html.append('	<ul>')
	for entry in channel.entries[:itemCount]:
		url = entry.id
		title = entry.title
		date = datetime.strptime(entry.date, "%a, %d %b %Y %H:%M:%S +0000");
		html.append('<li><p><strong><a href="%s">%s</a></strong>' % (url, title));
		html.append('%s</p></li>' % (date.strftime("%B %d, %Y")))

	html.append('	</ul>')
	html.append('</div>')
	return "\n".join(html)

Amazon S3 file uploads in Django

Monday, December 8th, 2008

Last time I had to use S3 uploads in Django was in 0.96 and I am happy to report that it is simpler than ever to upload files to S3 in Django. The recent storage-engine refactoring in Django 1.0 makes it a breeze to implement - no need for custom field types or anything like that.

David Larlet has put together a very good collection of Django storage-engines that looks very well-written compared to some naive implementations that I happened to stumble across elsewhere on the web. In addition, it includes a library for MogileFS - looks very exciting indeed and I hope I’ll have an opportunity (or reason rather) to play around with it one day!

To use, simply download Amazons S3 api, along with Larlet’s storage-engines (I just placed them in site-packages), and then your model would look something like the following:

import S3Storage

class UserProfile(models.Model):
    storage = S3Storage()
    profile_image = models.ImageField(upload_to='some-dir', storage=storage)

JSON serialization in Django

Thursday, November 27th, 2008

I’ve been quite busy lately, but I decided I’d share a few lines of code on how to do JSON (as well as XML) serialization in Django - its very simple.

I like the REST-based approach that Rails takes, so I kind of incorporated that in my code. It allows you to do something like this:

Regular view:
http://foo.com/banners/list/
JSON serialized data:
http://foo.com/banners/list/?format=json
XML serialized data:
http://foo.com/banners/list/?format=xml

On to the code:

# models.py
class Banner(models.Model):
	title = models.CharField(max_length=64)
	url = models.URLField()
	image = models.ImageField(upload_to='img/banners/')

# views.py
from django.core import serializers
from django.http import HttpResponse, QueryDict
from django.shortcuts import render_to_response

def request_format(request):
	default_format = 'http'
	q = QueryDict(request.META['QUERY_STRING'])
	format = q.get('format', default_format)
	if(format not in ['http','json','xml']):
		format = default_format
	return format

def list_banners(request):
	banners = Banner.objects.filter()
	format = request_format(request)
	if format == 'http':
		return render_to_response('list_banners.html', {'banners':banners})
	else:
		serialized_data = serializers.serialize(format, banners, fields = ('title', 'url', 'image'))
		return HttpResponse(serialized_data, mimetype='application/'+format)

Another reason to use Mako in Django

Sunday, August 24th, 2008

I have been contemplating switching to Mako templates for my Django projects for quite some time, partly because I found some of the template syntax in Django slightly awkward.

Today, a question about recursion support in Django templates came up on the #Django IRC channel, which made me think for a minute. Off the top of my head, I couldn’t think of a quick way of accomplishing this in Django, short of using a template tag. However, some examples that I had seen in Mako came to mind, which illustrates the flexibility and power of this templating language.

I decided I would put together some code just to showcase how it could work. The example I chose is to print an organizational chart using the simple Employee class below:

models.py:

class Employee(models.Model):
    name = models.CharField(max_length=64)
    reports_to = models.ForeignKey('self', related_name='employees')

organizational_chart.html:

<ul>
    ${render_employee(ceo)}
</ul>
<%def name="render_employee(person)">
    <li>${person.name}
    <ul>
    % for employee in person.employees:
         %{render_person(employee)}
    % endfor
    </ul>
    </li>
</%def>