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 ‘Python’ 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)

Ignoring deprecation warnings in Twill/Python

Tuesday, May 19th, 2009

I wrote a small command-line utility with Twill and Python that I use to scrape prices from our booking engine at work. The Python script worked fine when I ran it on my Mac/Python2.5 but Twill (or mechanize, to be exact) was giving deprecation warnings when I tried to run it on Windows/Python2.6.1.

C:\Users\Administrator\twill\other_packages\_mechanize_dist\_auth.py:14: DeprecationWarning: the md5 module is deprecated; use hashlib instead import re, base64, urlparse, posixpath, md5, sha, sys, copy
C:\Users\Administrator\twill\other_packages\_mechanize_dist\_auth.py:14: DeprecationWarning: the sha module is deprecated; use the hashlib module instead import re, base64, urlparse, posixpath, md5, sha, sys, copy

The solution is trivial; to supress deprecation warnings in python, just add an ingore filter before the line where you import Twill (or any other offending library). The alert reader may notice that the second part of the filterwarnings argument (’.*’) is a regular expression; this would allow you to tailor exactly which deprecation warnings you wish to propagate to the command-line.

import warnings
warnings.filterwarnings('ignore', '.*')

from twill.commands import *
import twill