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
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
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
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
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
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
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
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.
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
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!
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
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
(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
(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
(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
(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
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
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
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
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
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
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
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.
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.
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