An advanced forms presentation given with Miguel Araujo (maraujop) at DjangoCon 2011. The transcript and slides is aimed at getting into Django Core, and Jacob Kaplan-Moss has stated this is his plan.
Advanced Django Form Usage @pydanny / @maraujop Daniel Greenfeld • pydanny • Python & Django developer for Cartwheel Web / RevSys • Founded django-uni-form • Does Capoeira • Lives in Los Angeles with his Fiancé, Audrey Roy (audreyr) http://www.flickr.com/photos/pydanny/4442245488/ 2
Advanced Django Form Usage @pydanny / @maraujop Miguel Araujo • maraujop • Freelance Python developer • Co-lead of django-uni-form • Does Muay Thai • Lives in Madrid with his amazing girlfiend who does aerospace for a living • http://maraujop.github.com/ 3
Advanced Django Form Usage @pydanny / @maraujop Tons of technical content • We probably won’t have time for questions • Slides will be posted right after the talk • Too much content in the abstract • Special thanks to: • Brian Rosner • James Tauber • Frank Wiles 4
Advanced Django Form Usage @pydanny / @maraujop Fundamentals of Good Form Patterns • Zen of Python still applies • import this • Spartan programming als0 is important 5
Advanced Django Form Usage @pydanny / @maraujop Calling forms the easy way • Smaller, cleaner code makes our lives better • Line 5 of the Zen of Python • Flat is better than nested. • Aim for minimal boilerplate • So you can focus on your business logic 9
Advanced Django Form Usage @pydanny / @maraujop Standard views.py 12 def my_view(request, template_name='myapp/my_form.html'): if request.method == 'POST': form = MyForm(request.POST) # Form #1! if form.is_valid(): # nested if! do_x() return redirect('/') else: form = MyForm() # Form #2! return render(request, template_name, {'form': form}) Form #1 Form #2 Only 1 nested if, but real code gets much more complex Custom business goes here
Advanced Django Form Usage @pydanny / @maraujop Easy views.py 13 def my_view(request, template_name='myapp/my_form.html'): # sticks in a POST or renders empty form form = MyForm(request.POST or None) if form.is_valid(): do_x() return redirect('home') return render(request, template_name, {'form': form}) Single form Custom business goes here If no request.POST, then instantiated with None
Advanced Django Form Usage @pydanny / @maraujop def my_view(request, template_name='myapp/my_form.html'): # sticks in a POST or renders empty form form = MyForm(request.POST or None) if form.is_valid(): do_x() return redirect('home') return render(request, template_name, {'form': form}) Easy views.py • 6 lines of code instead of 9 • 33.3333333333% less code to debug • One form instantiation • less conditionals == less edge case insanity 14
Advanced Django Form Usage @pydanny / @maraujop pydanny made up statistics • 91% of all Django projects use ModelForms • 80% ModelForms require trivial logic • 20% ModelForms require complicated logic 18 Let’s try and make that easy
Advanced Django Form Usage @pydanny / @maraujop Classic views.py for ModelForm 20 def my_model_edit(request, slug=slug, template_name='myapp/my_model_form.html'): # I wouldn't call the variable model, because it's an instance of a model, it's confusing mymodel = get_object_or_404(MyModel, slug=slug) if request.method == 'POST': form = MyForm(request, instance=mymodel) if form.is_valid(): mymodel = form.save() mymodel.day_shown = datetime.datetime.now() # Do any extra model stuff here mymodel.save() return redirect('home') else: form = MyForm(instance=mymodel) return render(request, template_name, {'form': form, 'model': mymodel}) Form #1 Form #2 Only 1 nested if, but real code gets much more complex Custom business goes here
Advanced Django Form Usage @pydanny / @maraujop Classic views.py for ModelForm 21 def my_model_edit(request, slug=slug, template_name='myapp/my_model_form.html'): # I wouldn't call the variable model, because it's an instance of a model, it's confusing mymodel = get_object_or_404(MyModel, slug=slug) if request.method == 'POST': form = MyForm(request, instance=mymodel) if form.is_valid(): mymodel = form.save() mymodel.day_shown = datetime.datetime.now() # Do any extra model stuff here mymodel.save() return redirect('home') else: form = MyForm(instance=mymodel) return render(request, template_name, {'form': form, 'model': mymodel}) • 12 lines of code • Nested conditionals • What if we have to handle 3 different submit buttons? • Watch out for edge case insanity!
Advanced Django Form Usage @pydanny / @maraujop easy views.py + ModelForm 22 def my_model_edit(request, slug=slug, template_name='myapp/ my_model_form.html'): mymodel = get_object_or_404(MyModel, slug=slug) form = MyModelForm(request.POST or None, instance=mymodel) if form.is_valid(): mymodel = form.save() mymodel.edited_at_djangocon = True mymodel.save() return redirect('home') return render(request, template_name, {'form': form, 'mymodel': mymodel}) Single form Custom business goes here If no request.POST, then instantiated with None So this will fail validation!
Advanced Django Form Usage @pydanny / @maraujop add views.py + ModelForm 25 def my_model_add(request, template_name='myapp/my_model_form.html'): form = MyModelForm(request.POST or None) if form.is_valid(): mymodel = form.save() mymodel.added_at_djangocon = True mymodel.save() return redirect('home') return render(request,template_name,{'form': form,'mymodel':mymodel}) No need for an instance here This creates the model instance.
Advanced Django Form Usage @pydanny / @maraujop I can make it smaller! 26 def my_model_tiny_add(request,template_name='myapp/my_model_form.html'): form = MyModelForm(request.POST or None) if form.is_valid(): form.save() return redirect('home') return render(request,template_name,{'form':form,'mymodel':mymodel}) Good practice: Use model field defaults rather than doing it in your views. Not setting defaults here
Advanced Django Form Usage @pydanny / @maraujop Please don’t manually test your forms • This isn’t a testing talk but... • Forms are the number one thing to test • Don’t skip on testing them • Edge case insanity is the thing to fear 28
Advanced Django Form Usage @pydanny / @maraujop non-required to required • Your model fields are non-required • but you want the form fields to be required 31
Advanced Django Form Usage @pydanny / @maraujop Classic forms.py overload 33 class MyModelTooMuchTypingForm(forms.ModelForm): """ I've done this and it sucks hard to debug and too much duplication """ name = forms.CharField(_('Name'), max_length=50, required=True) age = forms.IntegerField(_('Age in years'), required=True) profession = forms.CharField(_('Profession'), required=True) bio = forms.TextField(_('Bio'), required=True) class Meta: model = MyModel class MyModel(models.Model): name = models.CharField(_('Name'), max_length=50, blank=True, null=True) age = models.IntegerField(_('Age in years'), blank=True, null=True) profession = models.CharField(_('Profession'), max_length=100, blank=True, null=True) bio = models.TextField(_('Bio'), blank=True, null=True) Nearly duplicated code
Advanced Django Form Usage @pydanny / @maraujop Better forms.py overload 34 class MyModelForm(forms.ModelForm): """ Much better and you are extending, not copy/pasting """ def __init__(self): super(MyModelForm, self).__init__(*args, **kwargs) self.fields['name'].required = True self.fields['age'].required = True self.fields['profession'].required = True self.fields['profession'].help_text = _("Hi professor") class Meta: model = MyModel Fields are in a dict-like object
Advanced Django Form Usage @pydanny / @maraujop Try it with inheritance! 35 class BaseEmailForm(forms.Form): email = forms.EmailField(_('Email')) confirm_email = forms.EmailField(_('Email 2')) class ContactForm(BaseEmailForm): message = forms.CharField(_('Message')) def __init__(self): super(ContactForm, self).__init__(*args, **kwargs) self.fields['confirm_email'].label = _('Confirm your email') self.fields['confirm_email'].description = _('We want to be absolutely \ certain we have your correct email address.')
Advanced Django Form Usage @pydanny / @maraujop Dynamically adding fields to a form 37 def my_view(request, template_name='myapp/my_model_form.html'): form = MyModelForm(request.POST or None) # Let's add a field on the go, needs to be done before validating it form.fields['favorite_color'] = forms.ChoiceField( label = "Which is your favorite color from these?", choices = (('blue', 'blue'), ('red', 'red'), ('green', 'green')), widget = forms.RadioSelect, required = True, ) if form.is_valid(): # Let's get user's favorite color, # you can do whatever you want with it favorite_color = form.cleaned_data['favorite_color'] form.save() return redirect('home') return render(request, template_name, {'form': form}) Form dictionary of fields Fields have to be added before the form.is_valid method is checked.
Advanced Django Form Usage @pydanny / @maraujop Constructor overrides forms.py 39 class MyModelForm(forms.ModelForm): def __init__(self, *args, **kwargs): extra = kwargs.pop('extra') super(UserCreationForm, self).__init__(*args, **kwargs) for i, question in enumerate(extra): self.fields['custom_%s' % i] = forms.CharField(label=question) def extra_fields(self): """ Returns a tuple (question, answer) """ for name, value in self.cleaned_data.items(): if name.startswith('custom_'): yield (self.fields[name].label, value) looping over ‘extra’ iterable
Advanced Django Form Usage @pydanny / @maraujop Constructor overrides views.py 40 def my_view(request, template_name='myapp/my_model_form.html'): form = MyModelForm(request.POST or None, extra=["What's your pet's name?"]) if form.is_valid(): # We can gather extra fields data doing for (question, answer) in form.extra_fields(): save_answer(request, question, answer) form.save() return redirect('home') return render(request, template_name, {'form': form}) Passing in a list of form field titles
Advanced Django Form Usage @pydanny / @maraujop class ItemFormSet(BaseFormSet): def __init__(self, numberItems, *args, **kwargs): super(ItemFormSet, self).__init__(*args, **kwargs) self.numberItems = numberItems def clean(self): # Don't bother validating the formset # unless each form is valid on its own if any(self.errors): return for form in self.forms: if not form.cleaned_data['your_choice'] == 'mod_wsgi': raise ValidationError(u'mod_wsgi is the way to go!') formsets.py 42 Forms must have ‘your_choice’ field that only accepts “mod_wsgi”
Advanced Django Form Usage @pydanny / @maraujop Things I want python to describe in forms • Different fieldsets within the same form • Various buttons • submit • reset 45
Advanced Django Form Usage @pydanny / @maraujop renders as divs 48 {% load uni_form_tags %} {% uni_form my_form my_form.helper %} Renders the HTML form with buttons and everything wrapped in a fieldset.
Advanced Django Form Usage @pydanny / @maraujop Easy to use! • Django’s widget parameter attrs expects a dictionary • Replaces the normal widgets with HTML5 ones 53 import floppyforms as forms class ExampleForm(forms.Form): username = forms.CharField( label='', widget = forms.TextInput( attrs={'placeholder': '@johndoe'}, ), )
Advanced Django Form Usage @pydanny / @maraujop Customizable widgets 54 import floppyforms as forms class OtherEmailInput(forms.EmailInput): template_name = 'path/to/other_email.html' name="{{ name }}" id="{{ attrs.id }}" placeholder="[email protected]" {% if value %}value="{{ value }}"{% endif %}> path/to/other_email.html forms.py
Advanced Django Form Usage @pydanny / @maraujop What we are getting in 1.4 • Form rendering will use templates instead of HTML in Python • Template based widgets • Template based form layout 59
Advanced Django Form Usage @pydanny / @maraujop forms refactor and django-uni-form • forms refactor layout “lives” in templates • django-uni-form layout “lives” in python • Different approaches, same objective 60
Advanced Django Form Usage @pydanny / @maraujop The docs on custom fields 62 “Its only requirements are that it implement a clean() method and that its __init__() method accept the core arguments mentioned above (required, label, initial, widget, help_text).” The docs seem to be wrong
Advanced Django Form Usage @pydanny / @maraujop How it really works • required • widget • label • help_text • initial • error_messages • show_hidden_initial • validators • localize 63 You don’t need to implement “clean” You do need to implement:
Advanced Django Form Usage @pydanny / @maraujop fields.py 66 class AMarkField(forms.Field): widget = TextInput default_error_messages = { 'not_an_a': _(u'you can only input A here! damn!'), } def __init__(self, **kwargs): super(AMarkField, self).__init__(**kwargs) def to_python(self, value): if value in validators.EMPTY_VALUES: return None if value != 'A': raise ValidationError(self.error_messages['not_an_a']) return value Only accepts upper case A as input
Advanced Django Form Usage @pydanny / @maraujop Not DRY 67 class MyModelForm(forms.ModelForm): mark = CharField() def clean_mark(self): mark_value = self.cleaned_data['mark'] if mark_value is not None or mark_value.upper() != 'A': raise ValidationError(_(u'Only input A here!')) return mark_value class ExampleForm(forms.Form): mark = CharField() def clean_mark(self): mark_value = self.cleaned_data['mark'] if mark_value is not None or mark_value.upper() != 'A': raise ValidationError(_(u'Only input A here!')) return mark_value
Advanced Django Form Usage @pydanny / @maraujop widgets.py 71 from django.forms.extras.widgets import MultiWidget class AddressWidget(MultiWidget): def __init__(self, attrs=None): widgets = (TextInput, TextInput) super(AddressWidget, self).__init__(widgets, attrs) def decompress(self, value): """ If called, value is a string should return a list """ if value: # parse stuff and return a list return value.split() return [None, None] Called when a form with MultiWidget is passed initial or instance values Two TextInput widgets!!!
Advanced Django Form Usage @pydanny / @maraujop MultiValue, MultiWidget Field • Clean does not work in the standard way • Every field validated with its corresponding widget value • Beware! run_validator does run but validate is called 74 class AlternativeAddressField(forms.MultiValueField): widget = AddressWidget def __init__(self, *args, **kwargs): fields = (forms.CharField(), forms.CharField()) super(AlternativeAddressField, self).__init__(fields, *args, **kwargs) def compress(self, data_list): return data_list Django Ticket #14184
Advanced Django Form Usage @pydanny / @maraujop def render(self, name, value, attrs=None): # HTML to be added to the output widget_labels = [ 'Address: ', 'Number: ' ] if self.is_localized: for widget in self.widgets: widget.is_localized = self.is_localized # value is a list of values, each corresponding to a widget # in self.widgets. if not isinstance(value, list): value = self.decompress(value) output = [] final_attrs = self.build_attrs(attrs) id_ = final_attrs.get('id', None) for i, widget in enumerate(self.widgets): try: widget_value = value[i] except IndexError: widget_value = None if id_: final_attrs = dict(final_attrs, id='%s_%s' % (id_, i)) # Adding labels output.append(widget_labels[i] % ('%s_%s' % (name, i))) output.append(widget.render(name + '_%s' % i, widget_value, final_attrs)) return mark_safe(self.format_output(output)) Custom Widget Output 77 So Easy!
Advanced Django Form Usage @pydanny / @maraujop Problems with Custom Widget Output • Overring render means indepth knowledge • Hard to do custom output with built-in widgets • You can’t call super.render and customize easily • Templates? Open Ticket #15667 79
Advanced Django Form Usage @pydanny / @maraujop Problems with Custom Widget Output • Oldest ticket in Django is related to forms (#23) • ComboField is broken • Validators are thought to validate simple values because of the way run_validators is coded 81
Advanced Django Form Usage @pydanny / @maraujop Validators • Imagine you have an input that get a list of emails seperated by spaces • Your widget returns: [“[email protected]”, “[email protected]”] • You want to run validators on all of them • There is an EmailValidator 83
Advanced Django Form Usage @pydanny / @maraujop Amazing stuff • Ticket #27 reported by Adrian Holovaty • Single form field for multiple database fields 85
Advanced Django Form Usage @pydanny / @maraujop This has been incredible • We’ve just scratched the surface • Keep your code clean - or you’ll regret it • Miguel is awesome 87
Advanced Django Form Usage @pydanny / @maraujop This has been incredible • We want to see this and more in the formal Django docs • We’ve scratched our own itch • There are hidden things that need to see the light of day • Danny holds the DjangoCon talks record 88