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

Using a text input for ManyToMany relations in Django

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)

2 Responses to “Using a text input for ManyToMany relations in Django”

  1. Igor Ganapolsky

    Hi,
    great post. Hey, are you the guy in the django irc room with an alias dev360? You helped me out last Sunday night. Walked me through a lot of questions with an application I was working on. Anyway, I wanted to give thanks. I remember you were trying to finish up a website (”redneck” you called it). Hope you got it done on time.

    Hit me up if you want. Maybe we’ll run into each other on the irc chat again.

    Regards,
    Igor

  2. Hi Igor,

    Yeah, that was me :) .. I remember you, you were trying to improve somebody else’s solution. Did it work out for you? Sorry for the late response by the way.

    I got the site up finally last week. It’s a bit of an SEO experiment - I’m waiting to see how it will fare. You can check it out at http://www.guncomparer.com/.

    Take care,

    Christian

Leave a Reply