Model Forms

The contact form is a common, but simple use of a website form. Another common use for forms is to collect information from the user and save the information in the database. Examples include entering your personal information for a membership application, your name and address details for a sales order, and filling out a survey.

Using forms for collecting and saving data from users is so common Django has a special form class to make creating forms from Django models much easier—model forms. With model forms, you create a Django model and then create a form that inherits from Django’s ModelForm class. As always, the devil is in the detail, so let’s create a form for our Venue model.

MyClub wants to allow venue managers to add their venue to the club database. We can’t give venue managers admin access to the backend, so we will create a form they can fill out in the frontend.

The process is as follows:

  1. Create the VenueForm model form for collecting venue information from the user.
  2. Add a new view to manage the form.
  3. Create the form template; and
  4. Add the new view and form to our urls.py file and update the site template to link to the venue form.

Create the Venue Form

Creating a form for the model is where the power of Django’s ModelForm class really shines, as it’s an almost trivial task. Create a new forms.py file in your events app and enter the following code (new file):

  1. # \myclub_root\events\forms.py
  2. 1 from django import forms
  3. 2 from django.forms import ModelForm
  4. 3 from .models import Venue
  5. 4
  6. 5 class VenueForm(ModelForm):
  7. 6 required_css_class = 'required'
  8. 7 class Meta:
  9. 8 model = Venue
  10. 9 fields = '__all__'

That’s it—a few lines of code is all Django needs to create a form for your model to show the necessary HTML on the page, validate your form fields, and pass form data to your view.

There are some things to note, however, so let’s look at those now:

  • Line 2. We import the ModelForm class, which does all the heavy lifting for us.
  • Line 3. We import our Venue model.
  • Line 5. Our VenueForm class inherits from ModelForm
  • Line 6. required_css_class is a handy ModelForm class option that adds a CSS class to our required fields. We will use this class to add an asterisk (*) to the required fields in the form template.
  • Line 7. The ModelForm class has an internal Meta class which we use to pass in the metadata options the ModelForm class needs to render our form:
    • Line 8. The model on which to base our form; and
    • Line 9. The model fields to render on the form. Here, we’re using the special __all__ value to tell Django to use all the form fields.

Add the Venue View

The view to add a venue builds on what we learned previously in the chapter. We will call the new view add_venue, so let’s add the view code to the views.py file in our events app (changes in bold):

  1. # \myclub_root\events\views.py
  2. 1 from django.shortcuts import render
  3. 2 from django.http import HttpResponse
  4. 3 from django.http import HttpResponseRedirect
  5. 4 from datetime import date
  6. 5 import calendar
  7. 6 from calendar import HTMLCalendar
  8. 7 from .models import Event
  9. 8 from .forms import VenueForm
  10. 9
  11. 10 # ...
  12. 11
  13. 12 def add_venue(request):
  14. 13 submitted = False
  15. 14 if request.method == 'POST':
  16. 15 form = VenueForm(request.POST)
  17. 16 if form.is_valid():
  18. 17 form.save()
  19. 18 return HttpResponseRedirect('/add_venue/?submitted=True')
  20. 19 else:
  21. 20 form = VenueForm()
  22. 21 if 'submitted' in request.GET:
  23. 22 submitted = True
  24. 23 return render(request,
  25. 24 'events/add_venue.html',
  26. 25 {'form': form, 'submitted': submitted}
  27. 26 )

At the top of the file, we’re importing HttpResponseRedirect from django.http (line 3) and the form (VenueForm) from forms.py (line 8). This view is functionally identical to the view for our contact form, except we have removed the code for emailing the form data and replaced it with the form.save() method to save the form data to our database (line 17).

Create the Venue Form Template

Now it’s time to create the template for our form. We will inherit from the site’s base template, so the form is very similar to the contact form template. Create a new file called add_venue.html and add it to the events app’s templates folder (new file):

  1. # \events\templates\events\add_venue.html
  2. 1 {% extends "base.html" %}
  3. 2
  4. 3 {% block title %}Add Venue{% endblock title %}
  5. 4
  6. 5 {% block content %}
  7. 6 <h1>Add your venue to our database</h1>
  8. 7
  9. 8 {% if submitted %}
  10. 9 <p class="success">
  11. 10 Your venue was submitted successfully. Thank you.
  12. 11 </p>
  13. 12 {% else %}
  14. 13 <form action="" method="post" novalidate>
  15. 14 <table>
  16. 15 {{ form.as_table }}
  17. 16 <tr>
  18. 17 <td>&nbsp;</td>
  19. 18 <td><input type="submit" value="Submit"></td>
  20. 19 </tr>
  21. 20 </table>
  22. 21 {% csrf_token %}
  23. 22 </form>
  24. 23 {% endif %}
  25. 24 {% endblock content %}

Except for some contextual changes to the display text, this template is identical to the contact form template, so I won’t go over any details.

While we are working on the form template, we need to add a little tweak to the main.css file, so our required field labels will have an asterisk appended to the label:

  1. # add to the end of \static\main.css
  2. .required label:after {
  3. content: "*";
  4. }

Link to the Add Venue Form

The last task to complete is to provide a new URL configuration for the events app and add an HTML anchor to the base template to link our website to the new add venue form.

First, let’s add the URL configuration to the events app’s urls.py file (changes in bold):

  1. # \myclub_root\events\urls.py
  2. 1 from django.urls import path, re_path
  3. 2 from . import views
  4. 3
  5. 4 urlpatterns = [
  6. 5 path('', views.index, name='index'),
  7. 6 path('add_venue/', views.add_venue, name='add-venue'),
  8. 7 # ...
  9. 8 ]

One change here—I’ve added a new path() function on line 6 which will redirect a URL ending with add_venue/ to the add_venue view.

And now for the base template (change in bold):

  1. # \myclub_site\templates\base.html
  2. 1 <aside id="rightsidebar">
  3. 2 <nav id="nav">
  4. 3 <ul>
  5. 4 <li><a href="/add_venue">Add Your Venue</a></li>
  6. 5 <li>Menu 2</li>
  7. 6 <li>Menu 3</li>
  8. 7 </ul>
  9. 8 </nav>
  10. 9 </aside>

Here, in line 4, I’ve added a simple HTML anchor tag so the first menu item on the right sidebar links to our new add venue form.

Save all your files and fire up the development server. When you open your site, there should be a link to add a venue in the right sidebar. Click this link, and you should see your new add a venue form (Figure 8-7).

Model Forms - 图1

Figure 8-7: The new form for adding a venue.

Overriding Form Methods

While our new form for adding venues is functional, it has a problem—both contact phone and contact email address fields are optional. This was OK in the admin because a staff member would know that MyClub needs at least one form of contact in the database. User experience design best-practice says you should never assume site visitors know how to fill out a form correctly. So, we need to add custom validation to ensure the person filling out the form enters either a phone number or an email address.

As a part of the validation process, both regular forms and model forms run the clean() method. You can override this special method to provide custom validation. To override the clean() method in a ModelForm class, we add the override to the class (changes in bold):

  1. # \myclub_root\events\forms.py
  2. 1 from django import forms
  3. 2 from django.forms import ModelForm
  4. 3 from .models import Venue
  5. 4
  6. 5 class VenueForm(ModelForm):
  7. 6 required_css_class = 'required'
  8. 7 class Meta:
  9. 8 model = Venue
  10. 9 fields = '__all__'
  11. 10
  12. 11 def clean(self):
  13. 12 cleaned_data = super().clean()
  14. 13 phone = cleaned_data.get("phone")
  15. 14 email_address = cleaned_data.get("email_address")
  16. 15 if not (phone or email_address):
  17. 16 raise forms.ValidationError(
  18. 17 "You must enter either a phone number or an email, or both."
  19. 18 )

Let’s have a closer look at the new method we’ve added to the VenueForm class:

  • Line 12. super().clean() maintains any validation logic from parent classes.
  • Lines 13 and 14. We’re retrieving the values for the venue phone number and email address from the dictionary saved in the cleaned_data attribute.
  • Line 15. If both the phone number and email address are blank, a validation error is raised (line 16).

You can see how this new custom validation works in practice if you try to submit the form with both the phone number and email address fields blank (Figure 8-8).

Model Forms - 图2

Figure 8-8: Overriding the clean() method of the Form class provides a painless way to add custom validation to your form.