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

Advanced Django Forms Usage

Advanced Django Forms Usage

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.

Daniel Greenfeld

September 26, 2011
Tweet

More Decks by Daniel Greenfeld

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