Share
Embed
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
Transcript
90 slides
-
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 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 -
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 -
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 -
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 A Basic Django
Form 11 class MyForm(forms.Form): name = forms.CharField(_('Name'), required=True) -
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 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 -
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 A Basic ModelForm
19 class MyModelForm(forms.Form): class Meta: model = MyModel fields = ['name'] -
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 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}) -
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 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 -
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 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) -
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. -
Can’t we just pass in a list of fields into
a form from a view? -
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 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 -
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 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 -
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 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> -
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 -
Advanced Django Form Usage @pydanny / @maraujop django-floppyforms • by
Bruno Renie @brutasse • HTML5 Widgets • Fully customizable templates • Plays nice with django-uni-form 52 -
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' <input type="email" 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 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 -
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: -
How the heck then do I actually add a custom
form field?!? -
Advanced Django Form Usage @pydanny / @maraujop How does form
validation work? 65 -
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 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 -
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 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) -
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 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 -
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! -
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 HTML5 tickets •
Ticket #16304 • Ticket #16630 • Just use django-floppyforms 82 -
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 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) -
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 -
Advanced Django Form Usage @pydanny / @maraujop Questions? 90 Daniel
Greenfeld & Miguel Araujo