Monday, 17 November 2008

Django form with preset defaults

I have just started to learn how to use the Django web application framework

It has excellent documentation including tutorials. With a framework that has enough flexibility to do most things not everything is explained for a beginner in the documentation.

The application I am using the framework to write is for setting up product order forms. Each order has a number of "peripherals" which are added to the product.

In order to experiment with framework features I also set up a test application which sets up "bags" (like orders) that contain "beans" (like peripherals)

The data model for the Bean Bag application looks like this:


class Bean(models.Model):
Name=models.CharField(max_length=30)
def __unicode__(self):
return self.Name

class Bag(models.Model):
iam=models.CharField(max_length=30)
stuff=models.ManyToManyField('Bean',through='Beanzd')
def __unicode__(self):
return self.iam

class Beanz(models.Model):
bean=models.ForeignKey(Bean)
bag=models.ForeignKey(Bag)
n=models.PositiveIntegerField(default=1)

The trick is to use the "through" option to mediate the Beans into the Bag. The mediating "Beanz" class also adds a quantity "n"

Django has a very clever system for adding CRUD functionality with very little work. Define the model, add a file with a few lines in it and you have a full featured create/read/update/delete to the database.



Then I thought.."what if I want to make the admin forms so I can set the default list of beans that are available for each bag?". I altered the model like this:


class Bean(models.Model):
Name=models.CharField(max_length=30)
list=models.BooleanField(default=True)
def __unicode__(self):
return self.Name


So there is a "list" field which implies that the Bean is listed by default on the form. But the problem is how to convince the admin page to do this. First I altered the model



def wah(self):
for i in (Bean.objects.filter(list=True)):
yield i.pk

class Beanz(models.Model):
q=wah()
bean=models.ForeignKey(Beans,default=q.next())
bag=models.ForeignKey(Bag)
n=models.PositiveIntegerField(default=1)



This didn't work for two reasons. First the model is not the admin form. I was causing a default in the wrong place. Second, the way that the form was created meant that a number of different instance objects were made from the class Beanz and they each made a new instance of
the "wah" iterator.



I found that the way to alter the form in the admin interface was to sub class the form that the admin page got to build the interface. This could be done in such a way that the default could be set. To get around the problem of the different instance objects continually reseting the iterator I used a singleton pattern

In the model I did this:


class Bingo(Singleton):
def wah(self):
for i in (Bean.objects.filter(list=True)):
yield i.pk
def __init__(self):
if not self.__dict__.__contains__('q'): self.q=self.wah()
def again(self):
try:
r=self.q.next()
except (StopIteration, NameError, UnboundLocalError,AttributeError):
self.q=self.wah()
r=self.q.next()
return r



class Beanz(models.Model):
bean=models.ForeignKey(Bean)
bag=models.ForeignKey(Bag)
n=models.PositiveIntegerField(default=1)



and in the admin interface I did this


from django.forms import ModelForm

class BagAdminForm(ModelForm):
def __init__(self,*args,**kwargs):
super(BagAdminForm,self).__init__(*args,**kwargs)
b=Bingo()
self.fields.get("bean").initial=b.again()

class Meta:
model = Beanz

class beandefaultinline(admin.TabularInline):
form= BagAdminForm
model=Beanz
extra=len(Bean.objects.filter(list=True))

class DefaultListingBag(admin.ModelAdmin):
inlines=(beandefaultinline,)

admin.site.register(Bag, DefaultListingBag)


The modifications to the model are perhaps a bit clumsy. The "Borg Idiom" pattern might be better than the Singleton. In the admin model note that super is called before the changes are made to the sub classed data in the instance. This is so that all the stuff in the object is set up and available. Here is a screen grab of the finished result, with default "beans" listed

0 comments: