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

Two Scoops of Django - Common Patterns for Forms

Vic
December 02, 2014

Two Scoops of Django - Common Patterns for Forms

Common Patterns for Forms & More Things About Forms

Vic

December 02, 2014
Tweet

More Decks by Vic

Other Decks in Programming

Transcript

  1. Outline • The Power of Django Forms • Pattern 1:

    Simple ModelForm With Default Validators • Pattern 2: Custom Form Field Validators in ModelForms • Pattern 3: Overriding the Clean Stage of Validation • Pattern 4: Hacking Form Fields (2 CBVs, 2 Forms, 1 Model) • Pattern 5: Reusable Search Mixin View • More Things to Know About Forms • Know How Form Validation Works
  2. The Power of Django Forms • Django forms are powerful,

    flexible, extensible and robust. • Powerful validation features • Package tips: • django-floppyforms - http://goo.gl/kTFgu9 • django-crispy-forms - http://goo.gl/JNmx5h • django-forms-bootstrap - http://goo.gl/nFpmQJ 4
  3. The Power of Django Forms • Probably using Django forms

    even if project doesn’t serve HTML. • The chapter goes explicitly into one of the best parts of Django: forms, models and CBVs working in concert. 5
  4. Pattern 1: Simple ModelForm With Default Validators (1/3) FlavorCreateView FlavorDetailView

    FlavorUpdateView FlavorDetailView (CreateView) (DetailView) (UpdateView) (DetailView) (Ch.9, subsection 9.5.1) 6
  5. Pattern 1: Simple ModelForm With Default Validators (2/3) # flavors/views.py

    from django.views.generic import CreateView, UpdateView from braces.views import LoginRequiredMixin from .models import Flavor class FlavorCreateView(LoginRequiredMixin, CreateView): model = Flavor fields = ('title', 'slug', 'scoops_remaining') class FlavorUpdateView(LoginRequiredMixin, UpdateView): model = Flavor fields = ('title', 'slug', 'scoops_remaining') 7
  6. Pattern 1: Simple ModelForm With Default Validators (3/3) • FlavorCreateView

    and FlavorUpdateView are assigned Flavor as their model. • Both views auto-generate a ModelForm based on the Flavor model. • Those ModelForms rely on the default field validation rules of the Flavor model. • Django gives us a lot of great defaults for data validation, but the defaults are never enough. 8
  7. Pattern 2: Custom Form Field Validators in ModelForms (1/9) •

    Target: title field across our project’s dessert app started with the word “Tasty”. • This can be solved with a simple custom field validation. 9
  8. Pattern 2: Custom Form Field Validators in ModelForms (2/9) #

    core/validators.py from django.core.exception import ValidationError def validate_tasty(value): """Raise a ValidationError if the value doesn't start with the word 'Tasty' """ if not value.startswith(u"Tasty"): msg = u"Must start with Tasty" raise ValidationError(msg) 10
  9. Pattern 2: Custom Form Field Validators in ModelForms (3/9) #

    core/models.py from django.db import models from .validators import validate_tasty class TastyTitleAbstractModel(models.Model): title = models.CharField(max_length=255, validators=[validate_tasty]) class Meta: abstract = True 11
  10. Pattern 2: Custom Form Field Validators in ModelForms (4/9) 12

    # flavors/models.py from django.core.urlresolvers import reverse from django.db import models from core.models import TastyTitleAbstractModel class Flavor(TastyTitleAbstractModel): slug = models.SlugField() scoops_remaining = models.IntegerField(default=0) def get_absolute_url(self): return reverse("flavor_detail", kwars={"slug": self.slug})
  11. Pattern 2: Custom Form Field Validators in ModelForms (5/9) •

    Work with any other tasty food-based models such as a WaffleCone or Cake model. • Any model that inherits from the TastyTitleAbstractModel class will throw a validation error if anyone attempts to save a model with a title that doesn’t start with ‘Tasty’. 13
  12. Pattern 2: Custom Form Field Validators in ModelForms (6/9) •

    What if we wanted to use validate_tasty() in just forms? • What if we wanted to assign it to other fields besides the title? 14
  13. Pattern 2: Custom Form Field Validators in ModelForms (7/9) #

    flavors/forms.py from django import forms from core.validators import validate_tasty from .models import Flavor class FlavorForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(FlavorForm, self).__init__(*args, **kwargs) self.fields["title"].validators.append(validate_tasty) self.fields["slug"].validators.append(validate_tasty) class Meta: model = Flavor 15 Not change
  14. Pattern 2: Custom Form Field Validators in ModelForms (8/9) #

    flavors/views.py from django.contrib import messages from django.views.generic import CreateView, UpdateView, DetailView from braces.views import LoginRequiredMixin from .models import Flavor from .forms import FlavorForm class FlavorActionMixin(object): model = Flavor fields = ('title', 'slug', 'scoops_remaining') @property def success_msg(self): return NotImplemented def form_valid(self, form): messages.info(self.request, self.success_msg) return super(FlavorActionMixin, self).form_valid(form) 16
  15. Pattern 2: Custom Form Field Validators in ModelForms (9/9) class

    FlavorCreateView(LoginRequiredMixin, FlavorActionMixin, CreateView): success_msg = "created" # Explicitly attach the FlavorFrom class form_class = FlavorForm class FlavorUpdateView(LoginRequiredMixin, FlavorActionMixin, UpdateView): success_msg = "updated" # Explicitly attach the FlavorFrom class form_class = FlavorForm class FlavorDetailView(DetailView): model = Flavor 17
  16. Pattern 3: Overriding the Clean Stage of Validation (1/6) •

    Some interesting use cases: • Multi-field validation • Validation involving existing data from the database that has already been validated. • Django provides a second stage and process for validating incoming data. 18
  17. Pattern 3: Overriding the Clean Stage of Validation (2/6) •

    Why Django provides more hooks for validation? • The clean() method is the place to validate two or more fields against each other, since it’s not specific to any one particular field. • The clean validation stage is a better place to attach validation against persistent data. Since the data already has some validation, you won’t waste as many database cycles on needless queries.
  18. Pattern 3: Overriding the Clean Stage of Validation (3/6) #

    flavors/forms.py from django import forms from flavors.models import Flavor class IceCreamOrderForm(forms.Form): slug = forms.ChoiceField("Flavor") toppings = forms.CharField() def __init__(self, *args, **kwargs): super(IceCreamOrderForm, self).__init__(*args, **kwargs) self.fields["slug"].choices = [ (x.slug, x.title) for x in Flavor.objects.all() ] def clean_slug(self): slug = self.cleaned_data["slug"] if Flavor.objects.get(slug=slug).scoops_remaining <= 0: msg = u"Sorry we are out of that flavor." raise forms.ValidationError(msg) return slug 20
  19. Pattern 3: Overriding the Clean Stage of Validation (4/6) •

    For HTML-powered views, the clean_slug() method in our example, upon throwing an error, will attach a “Sorry, we are out of that flavor” message to the flavor HTML input field. • This is a great shortcut for writing HTML forms!
  20. Pattern 3: Overriding the Clean Stage of Validation (5/6) #

    flavors/forms.py from django import forms from flavors.models import Flavor class IceCreamOrderForm(forms.Form): # ... def clean(self): cleaned_data = super(IceCreamOrderForm, self).clean() slug = cleaned_data.get("slug", "") toppings = cleaned_data.get("toppings", "") # Silly "too much chocolate" validation example if u"chocolate" in slug.lower() and \ u"chocolate" in toppings.lower(): msg = u"Your order has too much chocolate." raise forms.ValidationError(msg) return cleaned_data 22
  21. Pattern 3: Overriding the Clean Stage of Validation (6/6) •

    Tip: Common Fields Used In Multi-Field Validation • Strength of the submitted password. • If the email model field isn’t set to unique=True, whether or not the email is unique. 23
  22. Pattern 4: Hacking From Fields (2 CBVs, 2Forms, 1 Model)

    (1/6) • This pattern covers a situation where two views/ forms correspond to one model. • An example might be a list of stores, where we want each store entered into the system as fast as possible, but want to add more data such as phone number and description later. 24
  23. Pattern 4: Hacking From Fields (2 CBVs, 2Forms, 1 Model)

    (2/6) # stores/models.py from django.core.urlresolvers import reverse from django.db import models class IceCreamStore(models.Model): title = models.CharField(max_length=100) block_address = models.TextField() phone = models.CharField(max_length=20, blank=True) description = models.TextField(blank=True) def get_absolute_url(self): return reverse("store_detail", kwargs={"pk": self.pk}) 25
  24. Pattern 4: Hacking From Fields (2 CBVs, 2Forms, 1 Model)

    (3/6) # stores/forms.py from django import forms from .models import IceCreamStore class IceCreamStoreUpdateForm(forms.ModelForm): phone = forms.CharField(required=True) description = forms.TextField(required=True) class Meta: model = IceCreamStore 26 Duplicated
  25. Pattern 4: Hacking From Fields (2 CBVs, 2Forms, 1 Model)

    (4/6) # stores/forms.py # Call phone and description from the self.fields dict-like object from django import forms from .models import IceCreamStore class IceCreamStoreUpdateForm(forms.ModelForm): class Meta: model = IceCreamStore def __init__(self, *args, **kwargs): # Call the original __init__ method before assigning # field overloads super(IceCreamStoreUpdateForm, self).__init__(*args, **kwargs) self.fields["phone"].required = True self.fields["description"].required = True 27
  26. Pattern 4: Hacking From Fields (2 CBVs, 2Forms, 1 Model)

    (5/6) # stores/forms.py from django import forms from .models import IceCreamStore class IceCreamStoreCreateForm(forms.ModelForm): class Meta: model = IceCreamStore fields = ("title", "block_address") class IceCreamStoreCreateForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(IceCreamStoreUpdateForm, self).__init__(*args, **kwargs) self.fields["phone"].required = True self.fields["description"].required = True class Meta(IceCreamStoreCreateForm): fields = ("title", "block_address", "phone", "description") 28
  27. Pattern 4: Hacking From Fields (2 CBVs, 2Forms, 1 Model)

    (6/6) # stores/views.py from django.views.generic import CreateView, UpdateView from .forms import IceCreamStoreCreateForm from .forms import IceCreamStoreUpdateForm from .models import IceCreamStore class IceCreamCreateView(CreateView): model = IceCreamStore form_class = IceCreamStoreCreateForm class IceCreamUpdateView(UpdateView): model = IceCreamStore form_class = IceCreamStoreUpdateForm 29
  28. Pattern 5: Reusable Search Mixin View (1/5) • We’re going

    to cover how to reuse a search form in two views that correspond to two different models. • This example will demonstrate how a single CBV can be used to provide simple search functionality on both the Flavor and IceCreamStore models. 30
  29. Pattern 5: Reusable Search Mixin View (2/5) # core/views.py class

    TitleSearchMixin(object): def get_queryset(self): # Fetch the queryset from the parent's get_queryset queryset = super(TitleSearchMixin, self).get_queryset() # Get the q GET parameter q = self.request.GET.get("q") if q: # return a filtered queryset return queryset.filter(title__icontains=q) # No q is specified so we return queryset return queryset 31
  30. Pattern 5: Reusable Search Mixin View (3/5) # add to

    flavors/views.py from django.views.generic import ListView from core.views import TitleSearchMixin from .models import Flavor class FlavorListView(TitleSearchMixin, ListView): model = Flavor 32 # add to stores/views.py from django.views.generic import ListView from core.views import TitleSearchMixin from .models import IceCreamStore class IceCreamStoreListView(TitleSearchMixin, ListView): model = IceCreamStore
  31. Pattern 5: Reusable Search Mixin View (4/5) {# form to

    go into stores/store_list.html template #} <form action="" method="GET"> <input type="text" name="q"> <button type="submit">search</button> </form> 33 {# form to go into flavors/flavor_list.html template #} <form action="" method="GET"> <input type="text" name="q"> <button type="submit">search</button> </form>
  32. Pattern 5: Reusable Search Mixin View (5/5) • Mixin are

    a good way to reuse code, but using too many mixins in a single class makes for very hard-to-maintain code. • Try to keep our code as simple as possible. 34
  33. More Things to Know About Forms • Django forms are

    really powerful, but there are edge cases that can cause a bit of anguish. • If you understand the structure of how forms are composed and how to call them, most edge cases can be readily overcome. • Don’t disable Django’s CSRF protection. • https://docs.djangoproject.com/en/1.6/ref/contrib/csrf/ 35
  34. Know How Form Validation Works • Form validation is one

    of those areas of Django where knowing the inner working will drastically improve your code. • When we call form.is_valid(), a lot of things happen behind the scenes. 36
  35. Know How Form Validation Works 1. If the form has

    bound data, form.is_valid() calls the form.full_clean() method. 2. form.full_clean() iterates through the form fields and each field validates itself: A. Data coming into this field is coerced into Python via the to_python() method or raises a ValidationError. B. Data is validated against field-specific rules, including custom validators. Failure raises a ValidationError. C. If there are any custom clean_<field>() methods in the form, they are called at this time.
  36. Know How Form Validation Works 3. form.full_clean() executes the form.clean()

    method. 4. If it’s a ModelForm instance, form._post_clean() does the following: A. Sets ModelForm data to the Model instance, regardless of whether form.is_valid() is True or False. B. Calls the model’s clean() method. For reference, saving a model instance through the ORM does not call the model’s clean() method.
  37. Know How Form Validation Works # core/models.py from django.db import

    models class ModelFormFailureHistory(models.Model): form_data = models.TextField() model_data = models.TextField() 39
  38. Know How Form Validation Works # flavors/models.py import json from

    django.contrib import messages from django.core import serializers from core.models import ModelFormFailureHistory class FlavorActionMixin(object): @property def success_msg(self): return NotImplemented def form_valid(self, form): messages.info(self.request, self.success_msg) return super(FlavorActionMixin, self).form_valid(form) def form_invalid(self, form): """Save invalid form and model data for later reference.""" form_data = json.dumps(form.cleaned_data) model_data = serializers.serialize("json", [form.instance])[1:-1] ModelFormFailureHistory.objects.create( form_data=form_data, model_data= model_data ) return super(FlavorActionMixin, self).form_invalid(form) 40
  39. Know How Form Validation Works • form_invalid() is called after

    failed validation of a form with bad data. • When it called here in this example, both the cleaned form data and the final data saved to the database are saved as a ModelFormFailureHistory record. 41
  40. Reference • Two Scoops of Django - Best Practice for

    Django 1.6 • By Daniel Greenfeld and Audrey Roy 42