Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Advanced Django Forms Usage

Miguel Araujo
September 29, 2011

Advanced Django Forms Usage

DjangoCon.us 2011 talk about Django forms advanced features given by Miguel Araujo (@maraujop) and Daniel Greenfeld (@pydanny). Presentation video.

Miguel Araujo

September 29, 2011
Tweet

More Decks by Miguel Araujo

Other Decks in Programming

Transcript

  1. 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
  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
  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
  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
  5. Advanced Django Form Usage @pydanny / @maraujop Spartan Programming •

    Horizontal complexity • Vertical complexity (line count) • Token count • Character count • Variables • Loops • Conditionals 6 http://www.codinghorror.com/blog/2008/07/spartan-programming.html
  6. Advanced Django Form Usage @pydanny / @maraujop Spartan Programming •

    Horizontal complexity • Vertical complexity (line count) • Token count • Character count • Variables • Loops • Conditionals 7 http://www.codinghorror.com/blog/2008/07/spartan-programming.html
  7. Advanced Django Form Usage @pydanny / @maraujop Spartan Programming •

    Horizontal complexity • Vertical complexity (line count) • Token count • Character count • Variables • Loops • Conditionals 8 http://www.codinghorror.com/blog/2008/07/spartan-programming.html
  8. 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
  9. Advanced Django Form Usage @pydanny / @maraujop A Basic Django

    Form 11 class MyForm(forms.Form): name = forms.CharField(_('Name'), required=True)
  10. 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
  11. 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
  12. 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
  13. Advanced Django Form Usage @pydanny / @maraujop Easy views.py +

    files • 6 lines of code again! • Code donated by Audrey Roy 16 def my_view(request, template_name='myapp/my_form.html'): form = MyForm(request.POST or None, request.FILES or None) if form.is_valid(): do_x() return redirect('home') return render(request, template_name, {'form': form}) Request.POST or None Request.FILES or None
  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
  15. Advanced Django Form Usage @pydanny / @maraujop A Basic ModelForm

    19 class MyModelForm(forms.Form): class Meta: model = MyModel fields = ['name']
  16. 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
  17. 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!
  18. 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!
  19. Advanced Django Form Usage @pydanny / @maraujop easy views.py +

    ModelForm • 9 lines of code instead of 12 • One conditional • clear and succinct code 23 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})
  20. 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.
  21. 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
  22. 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
  23. Advanced Django Form Usage @pydanny / @maraujop Django unit tests!

    29 def test_add_package_view(self): url = reverse('my-url') response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'package/package_form.html') for c in Category.objects.all(): self.assertContains(response, c.title) count = Package.objects.count() response = self.client.post(url, { 'category': Category.objects.all()[0].pk, 'repo_url': 'http://github.com/django/django', 'slug': 'test-slug', 'title': 'TEST TITLE', }, follow=True) self.assertEqual(Package.objects.count(), count + 1) self.assertContains(response, "Django") POSTing a form follow=True assertContains is your best friend
  24. 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
  25. Advanced Django Form Usage @pydanny / @maraujop A basic Django

    models.py 32 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)
  26. 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
  27. 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
  28. 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.')
  29. 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.
  30. 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
  31. 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
  32. 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”
  33. Advanced Django Form Usage @pydanny / @maraujop views.py 43 from

    django.forms.models import formset_factory def wsgi_form_view(request): WsgiFormset = formset_factory(ExampleForm, extra=5, formset=ItemFormSet) formset = WsgiFormset(bogus_number, request.POST, request.FILES) Truncated formset view
  34. 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
  35. Advanced Django Form Usage @pydanny / @maraujop Programmatic layouts 47

    class ExampleForm(forms.Form): def __init__(self, *args, **kwargs): self.helper = FormHelper() self.helper.layout = Layout( Fieldset( 'first arg is the legend of the fieldset', 'like_website', 'favorite_number', ), Fieldset( 'second arg is the legend of the fieldset', 'favorite_color', 'favorite_food', ) ButtonHolder( Submit('submit', 'Submit', css_class='button white') ) ) return super(ExampleForm, self).__init__(*args, **kwargs) from uni_form.helpers import FormHelper, Submit, Reset from uni_form.helpers import Fieldset, ButtonHolder, Layout Fieldset Button Holder Button Layout FormHelper
  36. 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.
  37. Advanced Django Form Usage @pydanny / @maraujop Sample output 49

    <form action="#" class="uniForm"> <fieldset class="inlineLabels"> <div class="ctrlHolder"> <label for="name">Name</label> <input type="text" id="name" name="name" value="" size="35" class="textInput"/> </div> <div class="ctrlHolder"> <label for="email">Email</label> <input type="text" id="email" name="email" value="" size="35" class="textInput"/> </div> <div class="ctrlHolder"> <label for="comment">Comment</label> <textarea id="comment" name="comment" rows="25" cols="25"></textarea> </div> <div class="buttonHolder"> <label for="tos" class="secondaryAction"><input type="checkbox" id="tos" name="tos"/> I agree to the <a href="#">terms of service</a></label> <button type="submit" class="primaryAction">Post comment</button> </div> </fieldset> </form>
  38. Advanced Django Form Usage @pydanny / @maraujop django-uni-form • Programmatic

    layout • Div based forms • Section 508 compliant • Fully customizable templates • http://django-uni-form.rtfd.org 50
  39. Advanced Django Form Usage @pydanny / @maraujop django-floppyforms • by

    Bruno Renie @brutasse • HTML5 Widgets • Fully customizable templates • Plays nice with django-uni-form 52
  40. 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'}, ), )
  41. Advanced Django Form Usage @pydanny / @maraujop Customizable widgets 54

    import floppyforms as forms class OtherEmailInput(forms.EmailInput): template_name = 'path/to/other_email.html' <input type="email" name="{{ name }}" id="{{ attrs.id }}" placeholder="[email protected]" {% if value %}value="{{ value }}"{% endif %}> path/to/other_email.html forms.py
  42. Advanced Django Form Usage @pydanny / @maraujop They get along

    well 57 class ListFriendsForm(forms.Form): username = forms.CharField( widget = forms.TextInput( attrs={'placeholder': '@maraujop'}, ), ) def __init__(self, *args, **kwargs): self.helper = FormHelper() self.helper.layout = Layout( Div( 'username', ) ButtonHolder( Submit('submit', 'Submit', css_class='button white') ) ) return super(ExampleForm, self).__init__(*args, **kwargs) No changes or special code needed
  43. 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
  44. 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
  45. 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
  46. 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:
  47. 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
  48. 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
  49. Advanced Django Form Usage @pydanny / @maraujop The JSON field

    69 class JSONField(forms.Field): default_error_messages = { 'invalid': 'This is not valid JSON string' } def to_python(self, value): if value in validators.EMPTY_VALUES: return None try: json = simplejson.loads(value) except ValueError: raise ValidationError(self.error_messages['invalid']) return json
  50. 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!!!
  51. Advanced Django Form Usage @pydanny / @maraujop fields.py 72 class

    AddressField(forms.Field): self.widget = AddressWidget def to_python(self, value): # Already gets a Python list return value isinstance(["921 SW Sixth Avenue", "Portland"], list) class ExampleForm(object): forms.CharField(widget=AddressWidget) isinstance("921 SW Sixth Avenue, Portland", str)
  52. 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
  53. Advanced Django Form Usage @pydanny / @maraujop Validators with Multi-Field

    75 class AlternativeAddressField(forms.MultiValueField): widget = AddressWidget def __init__(self, *args, **kwargs): fields = (forms.CharField(), forms.CharField()) super(AlternativeAddressField, self).__init__(fields, *args, **kwargs) def validate(self, value): self._run_validators(value) def compress(self, data_list): return data_list
  54. Advanced Django Form Usage @pydanny / @maraujop def render(self, name,

    value, attrs=None): # HTML to be added to the output widget_labels = [ '<label for="id_%s">Address: </label>', '<label for="id_%s">Number: </label>' ] 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!
  55. 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
  56. 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
  57. Advanced Django Form Usage @pydanny / @maraujop HTML5 tickets •

    Ticket #16304 • Ticket #16630 • Just use django-floppyforms 82
  58. 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
  59. Advanced Django Form Usage @pydanny / @maraujop MultiValidator 84 class

    MultiValidator(object): def __init__(self, validators): self.validators = validators def __call__(self, data_list): errors = [] for value in data_list: for validator in self.validators: try: validator(value) except ValidationError, e: if hasattr(e, 'code'): errors.append("FiXED MESSAGE") raise ValidationError(errors)
  60. Advanced Django Form Usage @pydanny / @maraujop Amazing stuff •

    Ticket #27 reported by Adrian Holovaty • Single form field for multiple database fields 85
  61. Fin

  62. 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
  63. 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
  64. Fin