forms, getting your money's worth
DESCRIPTION
These are the slides from my talk on advanced forms topics given at EuroDjangocon 2009.TRANSCRIPT
Forms, Getting Your Money's Worth
Alex Gaynor
What Are Forms For?•They are a way to get data from the user
django.forms•Encapsulate forms into objects•Know how to validate our data•In the case of ModelForms, they know how to save data•Formsets: Multiple of the same type of Form•Understand part of the idea of the HTTP request/response cycle
The 20 second recapfrom django import forms
from myapp.models import MyModel
class MyForm(forms.Form): my_field = forms.CharField() other_field = forms.BooleanField(widget=forms.Select)
class MyModelForm(forms.ModelForm): class Meta: model = MyModel
Going beyond the basics•What you just saw is how 90% of people use django.forms•That's using classes to define static forms that don't change•But that's not how most applications are•What are some common tasks that aren't static, or otherwise handled by this setup?
Problems to Solve•Multiple forms of different types•Altering a form's options dynamically•Building entire forms dynamically
Multiple Forms•Formsets let us handle multiple forms of one type, for example letting a user enter multiple books•We can manually pass around multiple forms
user_form = UserForm()profile_form = UserProfileForm()return render_to_response({ 'user_form': user_form, 'profile_form': profile_form,})
Doing a bit Better•That's really ugly, doesn't scale well•What would be easier?•One class to hold all of our forms•It should act like it's just a normal form object where possible•How do we build this?•Normal python tools of the trade, dynamically creating classes
def multipleform_factory(form_classes, form_order=None):
if form_order: form_classes = SortedDict([ (prefix, form_classes[prefix]) for prefix in form_order]) else: form_classes = SortedDict(form_classes) return type('MultipleForm', (MultipleFormBase,), {'form_classes': form_classes})
How type() workstype(object) -> returns the class of the object,type(1) == inttype([]) == list
type(name, bases, attrs) -> returns a new class named {{ name }}, inherits from {{ bases}}, and has attributes of {{ attrs}}
class MyForm(forms.Form): my_field = forms.CharField()
type('MyForm', (forms.Form,), {'my_field': forms.CharField()}
class MutlipleFormBase(object): def __init__(self, data, files): self.forms = [form_class(data, files, prefix=prefix) for form_class, prefix in self.form_classes.iteritems()]
def as_table(self): return '\n'.join([form.as_table() for form in self.forms])
def save(self, commit=True): return tuple([form.save(commit) for form in self.forms]) def is_valid(self): return all(form.is_valid() for form in self.forms)
How it works•Simple factory function creates a new class that subclasses MultipleFormBase, gives it an extra attribute, a dict of form classes•MultipleFormBase just emulates the API of Form and ModelForm, aggregating the methods as appropriate
class UserForm(forms.ModelForm): class Meta: model = User
class ProfileForm(forms.ModelForm): class Meta: model = UserProfile
UserProfileForm = multipleform_factory({ 'user': UserForm, 'profile': ProfileForm,}, ['user', 'profile'])
The Result•Now we can use UserProfileForm the same way we would a regular form.•We need to expand MultipleFormBase to implement the rest of the Form API, but these additional methods are trivial.
A More Dynamic Form•Not all users should see the same form•Simple example: a user should only be able to select an option from a dropdown if it belongs to them•Tools of the trade: Subclassing methods
class ObjectForm(forms.ModelForm): def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super(ObjectForm, self).__init__(*args, **kwargs) self.fields['related'].queryset = \ self.fields['related'].queryset. \ filter(owner=user) class Meta: model = Object
Dead Simple•Now when we instantiate our form we just need to prive a user kwarg.•Form and ModelForm are just python. We overide methods to add our own behavior, then call super().•We can apply this same technique anywhere in Python, to any method.
Building it Dynamically•What if we don't just want to alter our form per request, what if we want to build the whole thing dynamically?•Common example: a survey application where we keep the survey questions in the database.•Let's build it.
The Pieces•What do we need to build this survey application?•A Model for a Survey and another for Fields on each Form(we're keeping it simple).•A function to take a Survey object and turn it into a Form subclass.
class Survey(models.Model): name = models.CharField(max_length=100)
FIELD_TYPE_CHOICES = ( (0, 'charfield'), (1, 'textfield'), (2, 'boolean'),)
class Question(models.Model): survey = models.ForeignKey(Survey, related_name='questions') label = models.CharField(max_length=255) field_type = models.IntegerField(choices= FIELD_TYPE_CHOICES)
FIELD_TYPES = { 0: forms.CharField, 1: curry(forms.CharField, widget=forms.Textarea) 2: forms.BooleanField}
def form_form_survey(survey): fields = {} for question in survey.questions.all(): fields[question.label] = FIELD_TYPES[question.field_type]( label = question.label ) return type('%sForm' % survey.name, (forms.Form,), fields)
What do We Have•2 very simple models for storing our surveys and the questions for these surveys•a way to create actual forms.Form classes for a survey•now we can use these forms in our views the same as normal
What Don't We have•Handling for more complex field types(such as choices)•A way to create Surveys(other than the admin, which would actually work really well for this)•A way to save the results of a survey•Consider these an excersise for the reader
Recap•Use standard Python to extend what we have•Don't be afraid to build your own things on top of what we already have•It's just Python
Questions?
P.S.: If you're someone who needs multiple datbase support in Django
come talk to me