Chapter 5: Django Forms

In programming, a form is an object that contains input fields, drop-down boxes, radio buttons, checkboxes, and a submit button. The duty of the form is to capture information from the user; what is done after that can be anything including storing that information in a database, sending an email, or generating a reporting document using that data. In this chapter, we discuss as much as we can about how forms are used in Django. Form objects are a very complex subject to discuss; we only have enough room in this chapter to cover the essentials and some advanced topics. Some of the topics in this chapter can be combined with other topics covered later in Chapter 8, Working with the Django REST Framework, to create form objects on SPA-like pages.

In our first form, we will create a class called ContactForm and build an email field in three different ways. Later, when we render that form in the browser, we will observe how its behavior changes using those three different mechanisms. We want to watch how validation is performed on each email field and watch how the behavior differs among them. This will give us a better understanding of which mechanism is needed for the intended behavior that we wish to achieve as the outcome. We will even look into writing custom field classes and learn how they can benefit us in the long run.

Depending on your project's needs, validation of your form may require a lot of custom JavaScript in order to accomplish your goals. The focus of this book is not on JavaScript but rather on the concepts of Django. However, by the end of this chapter, we will have provided an example that demonstrates how JavaScript can be blended into your project when working with dynamic inline formsets on a form object. You can expand upon this to build your own custom JavaScript functions that suit your project's needs.

In this chapter, we will cover the following:

  • Types of forms
  • Using form fields
  • Cleaning forms
  • Creating custom form fields
  • Working with form views
  • Rendering forms in templates
  • Linking models to a form
  • Adding inline formsets

Technical requirements

To work with the code in this chapter, the following tools will need to be installed on your local machine:

  • Python version 3.9 – used as the underlying programming language for the project
  • Django version 4.0 – used as the backend framework of the project
  • pip package manager – used to manage third-party Python/Django packages

We will continue to work with the solution created in Chapter 2, Project Configuration. However, it is not necessary to use the Visual Studio IDE. The main project itself can be run using another IDE or run independently using a terminal or command-line window from within the project root folder. This is where the manage.py file resides. Whatever editor or IDE you are using, a virtual environment will also be needed to work with the Django project. Instructions for how to create a project and virtual environment can be found in Chapter 2, Project Configuration. You will need a database to store the data contained in your project. PostgreSQL was chosen for the examples in the previous chapter; however, any database type that you choose for your project can be used to work with the examples in this chapter.

We will also be using data that is in the form of a Django fixture, provided in Chapter 3, Models, Relations, and Inheritance, in the subsection titled Loading the Chapter_3 data fixture. Make sure the chapter_3 fixture is loaded into your database; if this has already been done, then you may skip the next command. If you have already created the tables found in Chapter 3, Models, Relations, and Inheritance, and have not loaded that fixture yet, then run the following command, after activating your virtual environment:

(virtual_env) PS > python manage.py loaddata chapter_3

All of the code created in this chapter can be found in the GitHub repository for this book: https://github.com/PacktPublishing/Becoming-an-Enterprise-Django-Developer. The bulk of the code depicted in this chapter can be found in the /becoming_a_django_entdev/becoming_a_django_entdev/chapter_5/ directory.

Check out the following video to see the Code in Action: https://bit.ly/3xQQ2H3.

Preparing for this chapter

Start by creating a new app in your project called chapter_5 by following the steps discussed in Chapter 2, Project Configuration, in the subsection titled Creating a Django app. As discussed in that section, don't forget to change the value of the name = variable for your app class found in the /becoming_a_django_entdev/becoming_a_django_entdev/chapter_5/apps.py file to now point to the path where you installed your app. Be sure to also include this app in your INSTALLED_APPS variable found in the settings.py file. At the end of Chapter 4, URLs, Views, and Templates, we set DEBUG = False as part of an exercise. Be sure to set this back to DEBUG = True for the remainder of this book.

In the main urls.py file of the site, add the following path, which points to the URL patterns of this chapter that we will be creating:

# /becoming_a_django_entdev/urls.py
...
urlpatterns = [
    path(
        '',   
        include(
            'becoming_a_django_entdev.chapter_5.urls'
        )
    ),
]

Now that we've created the app for this chapter, let's begin using the Django admin site to manage the models created in Chapter 3, Models, Relations, and Inheritance.

Types of forms

Django is designed to simplify a great deal of work involved when handling forms. It does this by providing ways to render your form object as HTML and process data on form submission. There are a lot of different ways to use and work with form objects but they all start with a form class. Django provides two different classes for us to use, ModelForm and Form. The differences between the two are that one links directly to the tables in a database and the other does not. The ModelForm class, the one that links to a database, will automatically create fields and perform field validation based on the field constraints set within that model class, from the database level.

Form classes also use a Meta subclass, as was used on a model class in Chapter 3, Models, Relations, and Inheritance. There are other form classes that Django provides, such as BaseForm and BaseModelForm, which are used to write abstract base form classes, but these form classes are beyond the scope of this book. Other classes relate to inline formsets, which are basically forms within a form. By the end of this chapter, we will have inserted an inline formset onto the page when the form gets rendered and used JavaScript to add more of them when the user clicks a button.

Let's discuss importing and using the Form and ModelForm classes when creating a form class first.

Form class – Form

The Form class is used to create fields that don't link to a database. This is used when a form is sending an email or generating a PDF report, just to name a few examples.

Follow these steps to create your Form class:

  1. Create a file called forms.py in your /becoming_a_django_entdev/chapter_5/ directory.
  2. Inside this file, include the following code:

    # /becoming_a_django_entdev/chapter_5/forms.py

    from django.forms

    import Form

    class ContactForm(Form):

        pass

We will discuss working with fields in a moment, but let's discuss importing the ModelForm class next.

Form class – ModelForm

The ModelForm class is used when we want to create or modify data directly in a database. Each field is linked to a column of the table it represents. Additional fields can be created and used in ways that are not linked to your database. For instance, you could fire off an email that contains the data from an added field. This field could also be a comment, timestamp, or another type of hidden data field.

To create your ModelForm class, inside your existing /chapter_5/forms.py file, include the following class:

# /becoming_a_django_entdev/chapter_5/forms.py
from django.forms 
import Form, ModelForm
class VehicleForm(ModelForm):
    pass

Later in this chapter, we will link this class to the Vehicle model, created in Chapter 3, Models, Relations, and Inheritance.

Next, let's remove these pass statements and begin working with field arguments.

Using form fields

Similar to the standard model field classes introduced in Chapter 3, Models, Relations, and Inheritance, Django also provides a number of form field classes that are available to use. The difference is that a model field class works with the columns of a database and a form field class is used only as an input field within an HTML <form></form> object in a template.

The following table can be used as a cheat sheet to reference what fields are available when writing your Form and/or ModelForm classes:

Form fields also accept a variety of different field arguments that customize the behavior of each field. In the next section, we will use some of the field types in the preceding list to write fields on our form classes, discussing the different arguments that can be used.

For a complete breakdown of each of these field types, visit the official Django documentation on field classes and arguments, found here: https://docs.djangoproject.com/en/4.0/ref/forms/fields/.

Common field arguments

We will begin adding fields to a form class and introduce field arguments in this exercise. Field arguments are a way for us to set properties on a field.

To create your field, in your /chapter_5/forms.py file, add the import statement highlighted in the following code block, and in the same ContactForm class, add a field called full_name:

# /becoming_a_django_entdev/chapter_5/forms.py
from django 
import forms
from django.forms 
import Form, ModelForm
class ContactForm(Form):
    full_name = forms.CharField(
        label = 'Full Name',
        help_text = 'Enter your full name, first and last   name please',
        min_length = 2,
        max_length = 300,
        required = True,
        error_messages = {
            'required': 'Please provide us with a name to address you as',
            'min_length': 'Please lengthen your name, min 2 characters',
            'max_length': 'Please shorten your name, max 300 characters'
        }
    )

In the preceding example, we defined an HTML <input type="text"> object using the forms.CharField field class. A CharField object's default widget is an input type="text" field. The label argument lets us define the text that would render as <label for="my_field_id">My Form Field Label</label> of this field.

The help_text argument will render a <span class="helptext">{{ your_help_text_message</span> element, right after your input field in the Document Object Model (DOM).

Document Object Model

The DOM is an interface found in all browsers that presents HTML in a tree-like structure of nodes. The nodes represent objects in this tree, where the <span> or <input> nodes comprise a single object.

The min_length and max_length arguments are used among most field types; they define the minimum and maximum character count, respectively, allowed in the field. The required argument will define whether the field must contain a value in order to be valid. These will render as attributes of an <input type="text" maxlength="300" minlength="2" required="" /> object.

Next, let's discuss form validation a little bit more. In the next two subsections, we will cover the widget and validator arguments. For a complete breakdown of all the field arguments that are available and not covered, visit https://docs.djangoproject.com/en/4.0/ref/forms/fields/#core-field-arguments.

Field widgets

A field's widget argument allows us to define what kind of field to use, such as an input object of the date, email, password, or text type. This can also be a checkbox, radio button, drop-down select, or text area, to name a few examples. We don't have to specify the widget argument unless we want to change the default widget or override its initial properties.

Follow the next step to override the full_name field to render an input with the id, class, and placeholder attributes. The rendered output we hope to achieve should look like the following dummy code:

# Demo Code
<input type="text" name="full_name" id="full-name" class="form-input-class" placeholder="Your Name, Written By...">

In your /chapter_5/forms.py file, edit your full_name field, as shown:

# /becoming_a_django_entdev/chapter_5/forms.py
from django 
import forms
from django.forms 
import Form, ModelForm
class ContactForm(Form):
    full_name = forms.CharField(
        ...,
        widget = forms.TextInput(
            attrs = {
                'id': 'full-name',
                'class': 'form-input-class',
                'placeholder': 'Your Name, Written By...'
            }
        ),
    ),

If we changed the default widget of a field from forms.TextInput to something else, such as forms.EmailInput, that would render as <input type="email">. Changing forms.TextInput to forms.DateInput would render as <input type="date">. Using forms.TextArea would render as a <textarea></textarea> object instead. Of course, these are just some of the many different options that exist. For a complete breakdown of all the widgets available and how they can help you construct your fields, visit https://docs.djangoproject.com/en/4.0/ref/forms/widgets/.

Let's discuss using field validators next.

Field validators

When manually defining widgets, we sometimes have to write specific validation rules. For example, let's take a forms.EmailInput class; this would require adding validation rules that determine whether the value of the string the user provided is actually in [email protected] format and not some random string, such as IAmAString.

Follow these steps to create and validate an email field:

  1. In your /chapter_5/forms.py file, in the existing ContactForm, add the email_1 field shown here:

    # /becoming_a_django_entdev/chapter_5/forms.py

    ...

    from django

    import forms

    from django.forms

    import Form, ModelForm

    from django.core.validators

    import EmailValidator

    class ContactForm(Form):

        ...

        email_1 = forms.CharField(

            label = 'email_1 Field',

            min_length = 5,

            max_length = 254,

            required = False,

            help_text = 'Email address in [email protected] format.',

            validators = [

                EmailValidator(

                    'Please enter a valid email address'

                ),

            ],

            error_messages = {

                'min_length': 'Please lengthen your name, min 5 characters',

                'max_length': 'Please shorten your name, max 254 characters'

            }

        )

While fields can be manipulated using validator arguments in this way, Django tries to provide developers with options that minimize or reduce the amount of code that they need to write. For example, instead of writing the preceding example to enforce an email format on CharField, we could just use the EmailField class instead, which already enforces this rule for us. The EmailField class includes all of the logic and validation to handle an email field.

  1. To practice using the EmailField class, we will create an additional field to compare and contrast both code approaches. In your /chapter_5/forms.py file, in the same ContactForm class, add the email_2 field shown here:

    # /becoming_a_django_entdev/chapter_5/forms.py

    from django

    import forms

    from django.forms

    import Form, ModelForm

    ...

    class ContactForm(Form):

        ...

        email_2 = forms.EmailField(

            label = 'email_2 Field',

            min_length = 5,

            max_length = 254,

            required = True,

            help_text = 'Email address in [email protected] format for contacting you should we have questions about your message.',

            error_messages = {

                'required': 'Please provide us an email address should we need to reach you',

                'email': 'Please enter a valid email address',

                'min_length': 'Please lengthen your name, min 5 characters',

                'max_length': 'Please shorten your name, max 254 characters'

            }

        )

The difference between the code found in step 1 and step 2 is that we do not need to define a widget or validator argument when using an EmailField class to produce the same behavior as with a CharField class. The error message is now located in the error_messages argument using the email key, as shown.

For a complete breakdown of all of the validator classes and methods available, visit https://docs.djangoproject.com/en/4.0/ref/validators/. Let's practice cleaning forms next, which is just another way to perform validation.

Cleaning forms

We can perform validation on form fields in other ways as well. Within a form class, we can write methods that validate each field individually by writing them in this format: def clean_{{ form_field_name }}(). When doing this, only the value of the field that we are cleaning can be accessed. If we want to access other field values found in that form, we have to write a single def clean() method that will allow us to compare two fields against each other. For example, we could use the def clean() method to only require a field when another field's value is not empty.

The following two subsections break down these two concepts.

Method – clean_{{ your_field_name }}()

To clean an individual form field, follow these steps:

  1. In your /chapter_5/forms.py file, in the same ContactForm class, add a new field called email_3, as shown:

    # /becoming_a_django_entdev/chapter_5/forms.py

    from django

    import forms

    from django.forms

    import Form, ModelForm

    ...

    class ContactForm(Form):

        email_3 = forms.CharField(

            label = 'Email Using CharField and Using Clean Method',

            required = False,

            help_text = 'Email address in [email protected] format for contacting you should we have questions about your message.',

        )

  2. In that same ContactForm class, add the clean_email_3 method shown here:

    # /becoming_a_django_entdev/chapter_5/forms.py

    ...

    from django.core.exceptions

    import ValidationError

    from django.core.validators

    import (

        EmailValidator,

        validate_email

    )

    class ContactForm(Form):

        ...

        def clean_email_3(self):

            email = self.cleaned_data['email_3']

            if email != '':

                try:

                    validate_email(email)

                except ValidationError:

                    self.add_error(

                        'email_3',

                        f'The following is not a valid email address: {email}'

                    )

            else:

                self.add_error(

                    'email_3',

                    'This field is required'

                )

            return email

In the preceding example, we are importing the validate_email() method from the django.core.validators library, to determine whether the string is in email format. First, we are using a simple conditional statement to check whether the field has a value or not; if not, we are issuing an error message stating "This field is required". We are performing a validation check even though the email_3 field has a required argument set to False. This just illustrates another way we can do the same thing. If a value exists, we then wrap the validate_email() method in a Try/Except statement, and if validation fails, we are adding the "The following is not a valid email address: {{ field_value }}" error message.

The self.add_error() method provided in the Form and ModelForm classes accepts two arguments: the first argument is the name of the field and the second is your custom error message. Instead of using self.add_error('email_3', 'This field is required') to add error messages to a form, we can use the raise ValidationError('This field is required') class instead. Except, there's one problem: using this class will remove this field from the cleaned_data values list. That will work if you only use the clean_email_3() method by itself. If you wanted to access that same cleaned data within the def clean() method, you'd need to return the value in def clean_email_3(), as depicted in the last line of step 2 previously. Django will fire off the individual clean methods on each field before the clean() method is executed, saving that as the last method in the stack of cleaning methods. If your field value is not returned in the clean method specific to that field, we will not be able to access it when we need to later on.

Let's use the clean() method next.

Method – clean()

The clean() method is used to access all of the field data within a form, upon form submission. It is in this method that you could compare against the values of many fields before allowing a successful form submission. This next example will allow us to compare two fields against each other and raise one or more different field validation messages.

Follow these steps to configure your clean() method:

  1. In your /chapter_5/forms.py file, add another field to your ContactForm class called conditional_required, as shown:

    # /becoming_a_django_entdev/chapter_5/forms.py

    from django

    import forms

    from django.forms

    import Form, ModelForm

    ...

    class ContactForm(Form):

        conditional_required = forms.CharField(

            label = 'Required only if field labeled "email_3" has a value',

            help_text = 'This field is only required if the field labeled "email_3 Field" has a value',

            required = False,

        )

  2. In that same ContactForm class, add the following clean() method:

    # /becoming_a_django_entdev/chapter_5/forms.py

    from django

    import forms

    from django.forms

    import Form, ModelForm

    ...

    class ContactForm(Form):

        ...

        def clean(self):

            email = self.cleaned_data['email_3']

            text_field = self.cleaned_data[

                'conditional_required'

            ]

            if email and not text_field:

                self.add_error(

                    'conditional_required',

                    'If there is a value in the field labeled "email_3" then this field is required'

                )

In this clean() method, we are assigning the value of the email_3 field to the variable called email. Then, we are assigning the value of the conditional_required field to the variable called text_field. Using a simple conditional statement, we then check to see whether email has a value present, and if so, check whether text_field has a value present. If this condition is met, we then add the required error to the conditional_required field. Since we set the conditional_required field to use the required = False argument, if there is no value in the email_3 field, this field will not be required.

Let's move on to creating our own custom form fields next.

Creating custom form fields

Sometimes, the needs of the project outweigh the options that are provided to us. If a field class is not available by default, we have two options: create our own or use a third-party package where someone else has already written a field class for us.

Continuing with the same ContactForm class, we will demonstrate the differences between validation mechanisms by building a MultipleEmailField. This will be a single field, accepting a single string of emails, all separated by commas. Each email item will then be checked independently to see whether it is in a valid email string format. We will use the same validate_email() function as we did before to enforce this constraint.

Field class – Field

Django provides a class called Field found in the django.forms.fields library, used to construct custom field classes. Any of the options and methods found in this class can be overwritten as needed. For example, overriding the def __init__() method will provide a way to add, change, or remove field arguments, completely transforming how you work with these fields later on. We won't actually be overriding the __init__() method for this exercise; instead, we will be working with the to_python() and validate() methods. These will be the only two methods needed to perform the validation that we need on MultipleEmailField.

Follow these steps to write your Field class:

  1. Create a new file called fields.py in your /becoming_a_django_entdev/chapter_5/ folder.
  2. In that file, add the following MultipleEmailField class and import statements:

    # /becoming_a_django_entdev/chapter_5/fields.py

    from django.core.exceptions

    import ValidationError

    from django.core.validators

    import validate_email

    from django.forms.fields

    import Field

    from django.forms.widgets

    import TextInput

    class MultipleEmailField(Field):

        widget = TextInput

        default_validators = []

        default_error_messages = {

            'required': 'Default Required Error Message',

            'email': 'Please enter a valid email address or addresses separated by a comma with NO spaces'

        }

  3. If you want to use some of the validators provided by Django, use the default_validators option shown in the preceding code. This is where you will define what validator to use. We are using our own logic found in the validate method of the MultipleEmailField class and will not be using a default validator for what we are trying to achieve. You are welcome to use any of the validators that Django provides for your field classes, found in the django.core.validators library.
  4. The default_error_messages option is used to define the default messages for a field class. In the default_error_messages option shown previously, we are specifying two keys: required and email. These two keys will act as the default message used when a required field has been submitted without a value present and when a value does not meet the email string format. When specifying a default error message in the default_error_messages option, we no longer have to use the error_messages = {} argument of a field. It is still possible to use the error_messages argument on a field-by-field basis if we wanted to.
  5. In that same MultipleEmailField class, add the following to_python() method:

    # /becoming_a_django_entdev/chapter_5/fields.py

    ...

    class MultipleEmailField(Field):

        ...

        def to_python(self, value):

            if not value:

                return []

            value = value.replace(' ', '')

            return value.split(',')

The to_python() method is used to transform values into Python objects. This one in particular is written to transform a string into a list of emails, excluding the comma.

  1. In that same MultipleEmailField class, add the following validate() method:

    # /becoming_a_django_entdev/chapter_5/fields.py

    ...

    class MultipleEmailField(Field):

        ...

        def validate(self, value):

            super().validate(value)

            for email in value:

                try:

                    validate_email(email)

                except ValidationError:

                    raise ValidationError(

                        self.error_messages['email'],

                        code = 'email'

                    )

The validate() method checks each item in the list of emails to make sure it is in email format. We are also overriding the options provided in the Field class, such as the widget option shown in step 2. The default widget is TextInput. Since that is already what we need, we don't actually have to include it; it was provided in the preceding example for illustrative purposes. When writing your own custom field, you can replace TextInput with any of the Django widgets found in the django.forms.widgets library. If you want to take your field one step further, you could even write your own custom widget class, but this is beyond the scope of this book.

Let's work on using our custom field class next.

Using a custom field

To use the MultipleEmailField that we created in the previous subsection, in your /chapter_5/forms.py file, add the following import statement and add the multiple_emails field to ContactForm, as shown:

# /becoming_a_django_entdev/chapter_5/forms.py
...
from django.forms 
import Form, ModelForm
from .fields 
import MultipleEmailField
class ContactForm(Form):
    ...
    multiple_emails = MultipleEmailField(
        label = 'Multiple Email Field',
        help_text = 'Please enter one or more email addresses, each separated by a comma and no spaces',
        required = True,
    )
    ...

In the preceding example, we do not need to include any validation messages because we have already defined the messages we want in the MultipleEmailField class.

The arguments that are available are listed here:

These are the arguments included within the __init__() method of the Field class in the Django library. If we needed to use arguments such as min_length and max_length, as we did for the full_name field, we should have constructed the MultipleEmailField class using the CharField class instead of the Field class like we did in step 2 of the subsection titled Field class – Field of this chapter, as depicted here:

# Dummy Code
from django.forms.fields 
import Field, CharField
class MultipleEmailField(CharField):

The reason why we would want to use CharField instead of the Field class is that it extends the Field class and adds logic that includes the min_length and max_length arguments. Using this notion, you can extend any other field class, making available any of the unique arguments or behaviors of that class when writing your own custom class.

Next, let's take our contact form and use it with a view class to serve up our contact page.

Working with form views

A form view is just like any other view class, except that a form view class is designed to process and handle form objects and form submissions.

Django offers four main form view classes, listed here:

  • FormView
  • CreateView
  • UpdateView
  • DeleteView

These can all be found in the django.views.generic.edit library.

If we were to create a view to work with the ContactForm class that we created earlier, which does not relate to any models, we would use a simple FormView class. The other three classes can be used with forms that relate to models. They each serve a different purpose: to create, update, or delete records in a database. For example, CreateView will render a form containing blank or default values intended to create a record that does not exist yet. UpdateView uses a form that looks up an existing record, displays the values that exist for that record, and allows changes to be made. DeleteView will display to the user a prompt or confirmation page, asking the user whether they actually want to proceed with this task, then delete that record.

Let's use the FormView class to begin building a page that displays the ContactForm class object. We will be working with CreateView and UpdateView later in this chapter. For a complete breakdown of how to use all of these form view classes, visit https://docs.djangoproject.com/en/4.0/ref/class-based-views/generic-editing/.

View class – FormView

Let's start by constructing a class called FormClassView using Django's FormView class. This class will have three options that we will define, the first option being template_name, which is used to define the path of the HTML template that we are using. The second option is the form_class option, which is used to define the name of the form class that this view is going to process, that being the ContactForm class. The third option is success_url, which specifies a relative URL path to redirect the user to when the form is successfully submitted.

Follow these steps to configure your FormClassView class:

  1. Make sure you have a file called views.py in your /becoming_a_django_entdev/chapter_5/ folder. This is usually created automatically for you when a new Django app is created.
  2. In that same file, add the code shown here:

    # /becoming_a_django_entdev/chapter_5/views.py

    from django.views.generic.edit

    import FormView

    from .forms

    import ContactForm

    class FormClassView(FormView):

        template_name = 'chapter_5/form-class.html'

        form_class = ContactForm

        success_url = '/chapter-5/contact-form-success/'

Any of these options can use a callable to gain those values, such as the use of the reverse() function to specify success_url. An example of how this can be done is depicted here, but this is not part of the actual exercise:

# Dummy Code

from django.urls

import reverse

from django.views.generic.edit

import FormView

class FormClassView(FormView):

    ...

    def get_success_url(self, **kwargs):

        return reverse('pattern_name', args=(value,))

We won't actually need the callable shown here to formulate the success URL. All we need is the string representation of '/chapter_5/contact-form-success/'.

  1. Next, configure the URL pattern for http://localhost:8000/chapter-5/form-class/. If this file was not automatically created for you, create the /chapter_5/urls.py file and add the following form page pattern and import statement:

    # /becoming_a_django_entdev/chapter_5/urls.py

    from django.urls

    import re_path

    from django.views.generic

    import (

        TemplateView

    )

    from .views

    import FormClassView

    urlpatterns = [

        re_path(

            r'^chapter-5/form-class/?$',

            FormClassView.as_view()

        ),

    ]

  2. In that same file, add the following success pattern:

    # /becoming_a_django_entdev/chapter_5/urls.py

    ...

    urlpatterns = [

        ...,

        re_path(

            r'^chapter-5/contact-form-success/?$',

            TemplateView.as_view(

                template_name = 'chapter_5/contact-success.html'

            ),

            kwargs = {

                'title': 'FormClassView Success Page',

                'page_id': 'form-class-success',

                'page_class': 'form-class-success-page',

                'h1_tag': 'This is the FormClassView Success Page Using ContactForm',

            }

        ),

    ]

In this step, we added a second pattern to serve as the success page at http://localhost:8000/chapter-5/contact-form-success/. This success page will be used for all exercises in this chapter.

Now that we have a view class to work with and some basic options defined, let's explore what it takes to work with the different request methods.

HTTP request methods

Working with the FormView class in Django, there are two HTTP request methods: the GET and POST methods. The GET method is intended to render a form with blank or default values onto a page and wait for the user to fill out the form and submit it. Once the form has been submitted, the POST method will be executed.

GET

The get() method is just like any other GET method for a view class. It is the go-to method when the page first gets loaded.

Follow these steps to configure your FormClassView class's get() method:

  1. In the /chapter_5/views.py file, add the get() method to your existing FormClassView class using the following code:

    # /becoming_a_django_entdev/chapter_5/views.py

    ...

    from django.views.generic.edit

    import FormView

    from django.template.response

    import (

        TemplateResponse

    )

    class FormClassView(FormView):

        ...

        def get(self, request, *args, **kwargs):

            return TemplateResponse(

                request,

                self.template_name,

                {

                    'title': 'FormClassView Page',

                    'page_id': 'form-class-id',

                    'page_class': 'form-class-page',

                    'h1_tag': 'This is the FormClassView Page Using ContactForm',

                    'form': self.form_class,

                }

            )

In the preceding get() method, we are returning an HTTP response in the form of a TemplateResponse class, using the value of self.template_name as the path to the template location. We are providing that template with context unique to this page, such as the title, page_id, page_class, h1_tag, and form variables that are depicted in the preceding code block. The value of self.form_class is used to pass the form object into the template. Initial values can be defined on form fields when the page first gets loaded, which is when the form is initialized.

  1. Add the following initial list to your existing get() method of the FormClassView class and pass it into your return context, as highlighted in the following code block:

    # /becoming_a_django_entdev/chapter_5/views.py

    ...

    class FormClassView(FormView):

        def get(self, request, *args, **kwargs):

            initial = {

                'full_name': 'FirstName LastName',

                'email_1': '[email protected]',

                # Add A Value For Every Field...

            }

            return TemplateResponse(

                request, self.template_name, {

                    ...

                    'form': self.form_class(initial),

                }

            )

In this get() method, we added the initial variable as a list and then passed that list into the self.form_class(initial) object, which lets us set the initial value on fields, defining field names as the keys shown previously with their corresponding values.

POST

A post() method is used to render the same page when a form has been submitted. In this method, we can determine whether the form is valid and if so, we want to redirect it to a success URL. If the form is not valid, the page will reload with the values that the user entered into the fields and display any error messages that may exist. We can also alter or add to the context of a page using a post() method.

In your /chapter_5/views.py file, in the same FormClassView class, add the post() method shown here:

# /becoming_a_django_entdev/chapter_5/views.py
...
from django.views.generic.edit 
import FormView
from django.http 
import HttpResponseRedirect
from django.template.response 
import (
    TemplateResponse
)
class FormClassView(FormView):
    ...
    def post(self, request, *args, **kwargs):
        form = self.form_class(request.POST)
        if form.is_valid():
            return HttpResponseRedirect(
                self.success_url
            )
        else:
            return TemplateResponse(
                request, 
                self.template_name, 
                {
                    'title': 'FormClassView Page - Please Correct The Errors Below',
                    'page_id': 'form-class-id',
                    'page_class': 'form-class-page errors-found',
                    'h1_tag': 'This is the FormClassView Page Using ContactForm<br /><small class="error-msg">Errors Found</small>',
                    'form': form,
                }
            )

Inside the TemplateResponse return statement, the highlighted text where the context is written represents the context that has changed from the get() method to the post() method. In order to preserve the data that the user has entered into the form, request.POST must be passed into the form class, where self.form_class(request.POST) is highlighted in the preceding step. If we didn't pass request.POST into self.form_class(), then we would have rendered a blank form, as if we were using the get() method, upon first visiting this page.

Now that we have our view class written, we can work on the template that will render our form next.

Rendering forms in templates

Django offers five main ways to easily and quickly render a form object onto a page. The first three are to render a form using a paragraph, table, or list structure. The other two include the traditional way of rendering a form, which is based on the template in the django.forms.templates.django.forms library called default.html, and then a way to render your own template. New to Django 4.0 is the template_name option on all form classes. This option allows you to point to a template file where you can structure your own HTML formatting.

Follow these steps to render your form objects:

  1. Copy the base_template_1.html file that was created in Chapter 4, URLs, Views, and Templates, into your /becoming_a_django_entdev/chapter_5/templates/chapter_5/base/ folder. Copy all related partial template files that are added as {% include %} statements into that file as well.
  2. That base_template_1.html file will be repurposed as the base template for this chapter's exercise. Adjust any paths to point to the new chapter_5 folder, such as any CSS and JavaScript file paths.
  3. Copy all related CSS, JavaScript, and HTML files into your chapter_5 app as well. These are not required to complete this exercise but will prevent 404 errors in your console logs.
  4. Create a new file called form-class.html inside your /becoming_a_django_entdev/chapter_5/templates/chapter_5/ folder and include the tags we can see in the following code block:

    # /becoming_a_django_entdev/chapter_5/templates/chapter_5/form-class.html

    {% extends 'chapter_5/base/base_template_1.html' %}

    {% load static %}

    {% block page_title %}{{ title }}{% endblock %}

    {% block head_stylesheets %}{% endblock %}

    {% block js_scripts %}{% endblock %}

    {% block page_id %}{{ page_id }}{% endblock %}

    {% block page_class %}{{ block.super }} {{ page_class }}{% endblock %}

  5. Inside your /chapter_5/form-class.html file, add the following code inside the body_content block, to render a form in the simplest way possible:

    # /becoming_a_django_entdev/chapter_5/templates/chapter_5/form-class.html

    ...

    {% block body_content %}

        ...

        <form method="post">

            {% csrf_token %}

            {{ form }}

            <input type="submit" value="Send Message">

        </form>

    {% endblock %}

Here, we need to at least write the <form> element and define an attribute of method="post" telling the browser how to handle a form submission. The {{ form }} tag renders any fields that exist for this form, using the django.forms.templates.django.forms.default.html library template. {% csrf_token %} is a cross-site request forgery token that is used for security measures and is required on all Django forms. <input type="submit"> specifies the button that is used to trigger the form's submit action.

Let's dive into the remaining four mechanisms and how they are used.

Render form – as_p

This option will render each field wrapped in a paragraph, <p></p>, element. The label will be stacked above the input field with the help text below it. If errors are present, each field will have its own list object rendered above the paragraph objects, listing all the errors relating to that field.

To render your form with as_p, in your /chapter_5/form-class.html file, change the {{ form }} tag to {{ form.as_p }}.

This should render each field to look like the demo code depicted here:

# Dummy code rendered, for first_name field
<ul class="errorlist">
    <li>Please provide us with a name to address you as</li>
</ul>
<p>
    <label for="full-name">Full Name:</label>
    <input type="text" name="full_name" id="full-name" class="form-input-class field-error" placeholder="Your Name, Written By..." maxlength="300" minlength="2" required="">
    <span class="helptext">Enter your full name, first and last name please</span>
</p>

Let's render the form formatted as a table next.

Render form – as_table

This option takes each field and wraps it in a <tr></tr> element. The label of this field is wrapped in a <th></th> element and the field itself is wrapped in a <td></td> element. The label will be stacked to the left with the input object and help text and error message displayed to the right, as shown:

Figure 5.1 – Render form – as_table

Figure 5.1 – Render form – as_table

To use this option, we still have to wrap the form tag in a <table></table> element because only the inner contents of a table are rendered.

To render your form as a table, in your /chapter_5/form-class.html file, change your {{ form }} tag to {{ form.as_table }} and wrap it in the <table> tag, as shown:

# /becoming_a_django_entdev/chapter_5/templates/chapter_5/form-class.html
...
        <table>
            {{ form.as_table }}
        </table>
...

Let's render the form formatted as a list next.

Render form – as_ul

This option will render your form as a list with each field wrapped in a <li></li> element. Inside that element, the label will come first, then the input field, and the help text last. If an error occurs, it gets injected as its own list item above that field, as shown:

Figure 5.2 – Render form – As a list

Figure 5.2 – Render form – As a list

We also have to wrap the form in an element with the <ul></ul> list element. To render your form as a list, in your /chapter_5/form-class.html file, change the {{ form }} tag to {{ form.as_ul }} and wrap it in the <ul> tag, as shown:

# /becoming_a_django_entdev/chapter_5/templates/chapter_5/form-class.html
...
        <ul>
            {{ form.as_ul }}
        </ul>
...

Let's use a new approach that was introduced in Django 4.0 next.

Render form – using template_name

New to Django 4.0 is the template_name feature. This feature is used to render a form in the style written within a custom template. It gives developers the ability to structure their own HTML when fields are rendered. Developers can create many different template styles and use them as needed. Fields are accessed via the {{ fields }} tag inside that custom template.

Follow these steps to configure your custom form template:

  1. In your /chapter_5/forms.py file, add the template_name option to the existing ContactForm class, as highlighted:

    # /becoming_a_django_entdev/chapter_5/forms.py

    ...

    from django.forms

    import Form, ModelForm

    ...

    class ContactForm(Form):

        template_name = 'chapter_5/forms/custom-form.html'

    Note

    Make sure that in your /chapter_5/form-class.html file, you render your form using only the basic {{ form }} tag and not any of the other preconfigured form rendering methods.

  2. Next, create the custom-form.html file in your /chapter_5/templates/chapter_5/forms/ folder. We will not be adding an {% extends %} tag to this file; instead, we will treat it as if we are using an {% include %} tag, where it is just a snippet and not an entire page of HTML.
  3. Inside your /chapter_5/custom-forms.html file, add the following code:

    # /becoming_a_django_entdev/chapter_5/templates/chapter_5/forms/custom-forms.html

    {% load static %}

    {% for field, errors in fields %}

        <div class="field-box{% if errors %} error{% endif %}">

            <label for="{{ field.id_for_label }}">

                {% if field.field.required %}<span class="required">*</span>{% endif %}{{ field.label|safe }}

            </label>

            <div class="form-group">

                {{ field }}

                {{ errors|safe }}

                {% if field.help_text and field.help_text != '' %}

                    <span class="help-text">

                        {{ field.help_text|safe }}

                    </span>

                {% endif %}

            </div>

        </div>

    {% endfor %}

Here, we are looping through all of the fields with the {% for field, errors in fields %} tag shown in the preceding code block. We've added our own HTML structure using the <div>, <label>, and <span> elements. The field itself is rendered using the {{ field }} tag. Other information, such as the help text, is used in conjunction with the safe filter in {{ field.help_text|safe }}. The safe filter is used to make sure that any HTML contained in the string gets rendered as HTML objects and not printed as the string representation of that object.

Let's demonstrate all of these form renderings in action next.

Render demo

By now, we should have a working form. In your browser, visit the URL http://localhost:8000/chapter-5/form-class/, and you should see your form rendered onto the page. Through all of the examples provided using the ContactForm class, we should see six fields on this page. Here, we can see how an email field can behave differently as we interact with the form. For example, if we set the required argument in the email_1 field to equal False, we can submit the form with nothing entered into this field and it will succeed. In the field named email_2, we specified the required argument as equal to True. This added the required attribute to that input field, preventing the user from submitting that form. This means the user will never see the error message that we provided in the Django code. This route would require the use of JavaScript, such as the jQuery Validate library, to handle the error state and display an error message for us. Doing nothing would result in the browser handling the error state for us, and in Chrome, that would look as in the following screenshot:

Figure 5.3 – ContactForm email_2 field

Figure 5.3 – ContactForm email_2 field

However, on the field named email_3, we set the required argument to equal False, and in the clean method, we are performing the validation that checks whether the field has a value or not. This lets us submit the form and see the error message that was provided on postback, as shown:

Figure 5.4 – ContactForm email_3 field

Figure 5.4 – ContactForm email_3 field

Next, let's take Django forms a step further and start working with the models in a form class. We created a placeholder class for this, called VehicleForm, in the section titled Form class – ModelForm.

Linking a model to a form

Linking a model to a form without needing any special field rendering is fairly easy.

In your /chapter_5/forms.py file, add the following code to the existing VehicleForm class (remember to remove the pass statement that was added to this class earlier):

# /becoming_a_django_entdev/chapter_5/forms.py
...
from django.forms 
import Form, ModelForm
from ..chapter_3.models 
import Vehicle
class VehicleForm(ModelForm):
    class Meta:
        model = Vehicle
        fields = [
            'vin', 
            'sold', 
            'price', 
            'make', 
            'vehicle_model', 
            'engine',
        ]

In the preceding example, we don't have to create fields for this form. Django will automatically use the form field type associated with the model field type that we wrote for the Vehicle model, created in Chapter 3, Models, Relations, and Inheritance. If any field behavior needed modifying, we would write form fields the same way we did for ContactForm and then customize them as we see fit. The Meta subclass used here defines what model class we are using and the fields option specifies what fields of that model we want to include and in what order they should be included.

Note

Using fields = '__all__' will include all fields that exist for that model in the order that they were written for that model.

Let's work with the CreateView class next.

View class – CreateView

Using the VehicleForm class that we now have wired up to the Vehicle model, let's create a view that will render a form with no or default field values using the CreateView class. It will let us create a new vehicle record in the database when that form is successfully submitted.

Follow these steps to configure your CreateView class:

  1. In your /chapter_5/views.py file, add the following import statements and create the ModelFormClassCreateView class, as follows:

    # /becoming_a_django_entdev/chapter_5/views.py

    ...

    from django.http

    import HttpResponseRedirect

    from django.views.generic.edit

    import (

        ...,

        CreateView

    )

    from django.template.response

    import (

        TemplateResponse

    )

    from .forms

    import ContactForm, VehicleForm

    class ModelFormClassCreateView(CreateView):

        template_name = 'chapter_5/model-form-class.html'

        form_class = VehicleForm

        success_url = '/chapter-5/vehicle-form-success/'

  2. In that same ModelFormClassCreateView class, add the following get() method:

    # /becoming_a_django_entdev/chapter_5/views.py

    ...

    class ModelFormClassCreateView(CreateView):

        ...

        def get(self, request, *args, **kwargs):

            return TemplateResponse(

                request,

                self.template_name,

                {

                    'title': 'ModelFormClassCreateView Page',

                    'page_id': 'model-form-class-id',

                    'page_class': 'model-form-class-page',

                    'h1_tag': 'This is the ModelFormClassCreateView Class Page Using VehicleForm',

                    'form': self.form_class(),

                }

            )

  3. In that same ModelFormClassCreateView class, add the following post() method:

    # /becoming_a_django_entdev/chapter_5/views.py

    ...

    class ModelFormClassCreateView(CreateView):

        ...

        def post(self, request, *args, **kwargs):

            form = self.form_class(request.POST)

            if form.is_valid():

                vehicle = form.instance

                vehicle.save()

                return HttpResponseRedirect(

                    self.success_url

                )

            else:

                return TemplateResponse(

                    request,

                    self.template_name,

                    {

                        'title': 'ModelFormClassCreateView Page - Please Correct The Errors Below',

                        'page_id': 'model-form-class-id',

                        'page_class': 'model-form-class-page errors-found',

                        'h1_tag': 'This is the ModelFormClassCreateView Page Using VehicleForm<br /><small class="error-msg">Errors Found</small>',

                        'form': form,

                    }

                )

In this example, we gave this class the same two methods that we used earlier—get() and post(). These two methods work the same way as when used in a class constructed with the FormView class. In the get() method, we are passing just a blank form into the template as context using self.form_class(). In the post() method, we are once again passing request into the form to get the data that was submitted by the user, using form = self.form_class(request.POST). In that post() method, validation is performed using if form.is_valid(): and it will either redirect to a success page or refresh, serving up the form with the correct error messages. If the form is validated successfully, just before we perform the redirect, we are saving the form using vehicle.save(), the same way we did when we added data using the Django shell in Chapter 3, Models, Relations, and Inheritance.

  1. Create a URL pattern using the ModelFormClassCreateView class, add the following path to your /chapter_5/urls.py file, and include the following import statements:

    # /becoming_a_django_entdev/chapter_5/urls.py

    from django.urls

    import re_path

    from django.views.generic

    import (

        TemplateView

    )

    from .views

    import (

        FormClassView,

        ModelFormClassCreateView

    )

    urlpatterns = [

        ...,

        re_path(

            r'^chapter-5/model-form-class/?$',

            ModelFormClassCreateView.as_view()

        ),

    ]

  2. In that same /chapter_5/urls.py file, add the following success URL pattern:

    # /becoming_a_django_entdev/chapter_5/urls.py

    ...

    urlpatterns = [

        ...,

        re_path(

            r'^chapter-5/vehicle-form-success/?$',

            TemplateView.as_view(

                template_name = 'chapter_5/vehicle-success.html'

            ),

            kwargs = {

                'title': 'ModelFormClass Success Page',

                'page_id': 'model-form-class-success',

                'page_class': 'model-form-class-success-page',

                'h1_tag': 'This is the ModelFormClass Success Page Using VehicleForm',

            }

        ),

    ]

We added the success URL pattern for a vehicle form at http://localhost:8000/chapter-5/vehicle-form-success/.

  1. Next, construct your /chapter_5/model-form-class.html file the same way that we created the /chapter_5/form-class.html file.
  2. Now, visit the URL http://localhost:8000/chapter-5/model-form-class/, and if you are rendering your form using the standard {{ form }} tag, you should see the page looking as in the following screenshot:

Figure 5.5 – VehicleForm using ModelFormClassCreateView

Figure 5.5 – VehicleForm using ModelFormClassCreateView

Of course, this example is in its simplest form. If you wanted to use another format or a template of your own, you would follow the steps under the Rendering forms in templates section of this chapter.

View class – UpdateView

In this example, we need to create a URL pattern using a path converter to capture the ID of the vehicle record that is being looked up.

Follow these steps to configure your UpdateView class:

  1. In your /chapter_5/urls.py file, add the path shown here:

    # /becoming_a_django_entdev/chapter_5/urls.py

    from django.urls

    import re_path

    from .views

    import (

        ...,

        ModelFormClassUpdateView

    )

    ...

    urlpatterns = [

        ...,

        re_path(

            'chapter-5/model-form-class/(?P<id>[0-9])/?$',

            ModelFormClassUpdateView.as_view(),

            name = 'vehicle_detail'

        ),

    ]

This pattern will allow us to access a Vehicle record by ID, also known as a primary key, in a database. We specify the ID of the Vehicle that we want to look up in the URL itself, as in http://localhost:8000/chapter-5/model-form-class/2/.

  1. In your /chapter_5/views.py file, add the ModelFormClassUpdateView class and import statements shown here:

    # /becoming_a_django_entdev/chapter_5/views.py

    ...

    from django.http

    import HttpResponseRedirect

    from django.template.response

    import (

        TemplateResponse

    )

    from django.views.generic.edit

    import (

        ...,

        UpdateView

    )

    from .forms

    import VehicleForm

    from ..chapter_3.models

    import Vehicle

    class ModelFormClassUpdateView(UpdateView):

        template_name = 'chapter_5/model-form-class.html'

        form_class = VehicleForm

        success_url = '/chapter-5/vehicle-form-success/'

  2. In that same ModelFormClassUpdateView class, add the get() and post() methods shown here:

    # /becoming_a_django_entdev/chapter_5/views.py

    from django.template.response

    import (

        TemplateResponse

    )

    from django.views.generic.edit

    import (

        ...,

        UpdateView

    )

    from ..chapter_3.models

    import Vehicle

    ...

    class ModelFormClassUpdateView(UpdateView):

        ...

        def get(self, request, id, *args, **kwargs):

            try:

                vehicle = Vehicle.objects.get(pk=id)

            except Vehicle.DoesNotExist:

                form = self.form_class()

            else:

                form = self.form_class(instance=vehicle)

            return TemplateResponse(

                request,

                self.template_name,

                {

                    'title': 'ModelFormClassUpdateView Page',

                    'page_id': 'model-form-class-id',

                    'page_class': 'model-form-class-page',

                    'h1_tag': 'This is the ModelFormClassUpdateView Class Page Using VehicleForm',

                    'form': form,

                }

            )

        def post(self, request, id, *args, **kwargs):

            # Use the same code as we did for the ModelFormClassCreateView class

  3. Now, navigate to the URL http://localhost:8000/chapter-5/model-form-class/2/, and you should see the form preloaded with the values found in the database for that Vehicle, as depicted in the following screenshot:
Figure 5.6 – VehicleForm using ModelFormClassUpdateView

Figure 5.6 – VehicleForm using ModelFormClassUpdateView

We once again use the same two get() and post() methods. One minor difference in how we are writing the get() method here is that we are performing the Vehicle query. We use a try/except statement to determine whether the object exists in the database using vehicle = Vehicle.objects.get(pk=id). If it does not exist, we create the form object as a blank form using form = self.form_class(). If the Vehicle object is found, then we pass that instance into the form that we are initializing, using form = self.form_class(instance=vehicle). The post() method is written the same as what we wrote for ModelFormClassCreateView, except that we updated the context string variables to reflect this class name.

Note

When working with fields that have an attribute of unique = True and saving that object in a form using the UpdateView class, you may get a postback error message telling you that the object already exists. To get around this, try removing the unique attribute on your model and implementing your own clean() method to enforce that uniqueness. There are also several other approaches to solve this while keeping the unique attribute; all are rather difficult to implement and go beyond the scope of this chapter. Practice building a form to update the Engine class on your own that does not contain a unique field.

Let's add inline formsets next.

Adding inline formsets

An inline formset is a form within a form. It's a way to provide dynamic fields, for example, for additional personnel, comments, or objects. They are commonly used in combination with JavaScript code on the frontend to create or remove sets of fields as desired by the user. In the next exercise, we will expand upon the ModelFormClassCreateView class to add our inline formset. This formset will capture prospective buyer information, to capture the first and last name of that lead. We will create an Add Another button for the user to add as many prospective buyers as they would like to. JavaScript is used to control creating and/or deleting the new DOM objects. It will also update the Django management form data in the process. You can build upon this concept to make your form more robust with added fields and controls for the user to manipulate inline formsets.

Follow the steps in the following sections to get started with inline formsets.

Formset function – formset_factory

A formset factory is a controller that we use to register inline formsets.

Follow these steps to create your formset factory:

  1. In the /chapter_5/forms.py file, add the following ProspectiveBuyerForm class, which will act as the inline form, capturing the first and last name of the prospective buyer:

    # /becoming_a_django_entdev/chapter_5/forms.py

    from django

    import forms

    from django.forms

    import Form, ModelForm

    ...

    class ProspectiveBuyerForm(Form):

        first_name = forms.CharField(

            label = 'First Name',

            help_text = 'Enter your first name only',

            required = True,

            error_messages = {

                'required': 'Please provide us with a first name',

            }

        )

        last_name = forms.CharField(

            label = 'Last Name',

            help_text = 'Enter your last name only',

            required = True,

            error_messages = {

                'required': 'Please provide us with a last name',

            }

        )

We are doing nothing different in the ProspectiveBuyerForm class in the preceding code compared to what we did in ContactForm before. The same concepts and validation measures apply to the fields within an inline formset. Adjust the logic as necessary for your fields.

  1. In the same /chapter_5/forms.py file, register that form as formset_factory using the following example. Make sure to place the ProspectiveBuyerFormSet class below the ProspectiveBuyerForm class in this file:

    # /becoming_a_django_entdev/chapter_5/forms.py

    ...

    from django.forms

    import (

        ...,

        formset_factory

    )

    ...

    ProspectiveBuyerFormSet = formset_factory(

        ProspectiveBuyerForm,

        extra = 1

    )

In the preceding example, we registered the ProspectiveBuyerForm class in a formset factory called ProspectiveBuyerFormset, which we will use next in our view class. The extra = 1 argument is used to include only one instance of this formset when this formset_factory is first initialized. There are many other options available, and they are all explained in detail here: https://docs.djangoproject.com/en/4.0/topics/forms/formsets/.

Note

In this example, we are using a standard formset_factory for a form with fields that are not linked to a model. Formsets that do link to models would use the modelformset_factory() method to link form fields to a model in your database. When using that method, data is saved in the view class the same way as when we saved the VehicleForm data.

Let's use this inline formset in a view class next.

Using inline formsets in the view class

Follow these steps to use your newly created inline formset in a view class:

  1. In your /chapter_5/views.py file, in the existing ModelFormClassCreateView class, add a few minor adjustments to the existing get() method, as highlighted in the following code:

    # /becoming_a_django_entdev/chapter_5/views.py

    ...

    from django.http

    import HttpResponseRedirect

    from django.template.response

    import (

        TemplateResponse

    )

    from django.views.generic.edit

    import (

        ...,

        CreateView

    )

    from .forms

    import ..., ProspectiveBuyerFormSet

    from ..chapter_3.models

    import Vehicle

    class ModelFormClassCreateView(CreateView):

        ...

        def get(self, request, *args, **kwargs):

            buyer_formset = ProspectiveBuyerFormSet()

            return TemplateResponse(

                request,

                self.template_name,

                {

                    ...

                    'form': self.form_class(),

                    'buyer_formset': buyer_formset,

                }

            )

  2. In the same ModelFormClassCreateView class, add a few minor adjustments to the existing post() method, as highlighted in the following code:

    # /becoming_a_django_entdev/chapter_5/views.py

    ...

    class ModelFormClassCreateView(CreateView):

        ...

        def post(self, request, *args, **kwargs):

            form = self.form_class(request.POST)

            buyer_formset = ProspectiveBuyerFormSet(

                request.POST

            )

            if form.is_valid():

                ...

            else:

                return TemplateResponse(

                    request,

                    self.template_name,

                    {

                        ...

                        'form': form,

                        'buyer_formset': buyer_formset,

                    }

                )

In the preceding steps, we are passing the inline ProspectiveBuyerFormset formset into the template as an additional context variable called buyer_formset. This and the form object should always be thought of as entirely separate objects. The form and the formset can also be related if they are using ForeignKey, ManyToMany, or OneToOne model relations.

Let's render these inline formsets into a template next.

Rendering inline formsets in the template

To render your newly created inline formsets into a template file, in your /chapter_5/model-form-class.html file, include all of the nodes, class names, and IDs that exist, as shown in the following code:

# /becoming_a_django_entdev/chapter_5/templates/chapter_5/model-form-class.html
...
{% extends 'chapter_5/base/base_template_1.html' %}
{% load static %}
...
{% block body_content %}
    ...
    <form method="post" id="form">
        {% csrf_token %}
        {{ form }}
        {% if buyer_formset %}
            <h3>Prospective Buyers</h3>
            {{ buyer_formset.non_form_errors }}
            {{ buyer_formset.management_form }}
            {% for form in buyer_formset %}
                <div class="formset-container {{ buyer_formset.prefix }}">
                    <div class="first-name">
                        {{ form.first_name.label }}: {{ form.first_name }}
                    </div>
                    <div class="last-name">
                        {{ form.last_name.label }}: {{ form.last_name }}
                    </div>
                </div>
            {% endfor %}
        {% endif %}
        <button id="add-formset" type="button">Add Another Prospective Buyer</button>
        <input type="submit" value="Save Vehicle">
    </form>
{% endblock %}

The JavaScript we will soon write will depend on this structure, and as your structure changes, be sure to change your JavaScript as well. In the preceding code, the important parts of the form include the ID attribute of the form itself, called id="form". We will use that to target the form as a whole in the JavaScript we are going to write. A conditional is used to check whether the buyer_formset variable exists before we do anything with it. For example, if you wanted to serve up an instance of this page that has no formsets at all, then this conditional will prevent breakage.

An important feature to never forget to include is the management form data, which is added using the {{ buyer_formset.management_form }} tag. This will include important data that Django needs to process your inline formsets. We then loop through each form in the buyer_formset object using {% for form in buyer_formset %}. For each form that does exist, we wrap all of the internal HTML in a node called <div class="formset-container"></div>. This class is important as it will differentiate between each inline form when we work with JavaScript. Inside, you can structure your fields however you like. Lastly, outside of the loop, just before the submit button, we need to add a new <button> of type="button" to prevent accidentally submitting the form. Give that button an attribute of id="add-formset".

Now, visit the same URL that we went to before, to add a new vehicle at http://localhost:8000/chapter-5/model-form-class/. You should see a form resembling the following:

Figure 5.7 – VehicleForm inline formset

Figure 5.7 – VehicleForm inline formset

There will only be one instance of the prospective buyer for now. Next, we will add the JavaScript controls that let us add more instances to this form.

Dynamic inline formsets

Follow these steps to configure the JavaScript needed to let the user add more instances of an inline formset:

  1. In your /chapter_5/base/base_template_1.html file, there is already a reference to a JavaScript file in the <head> of that document. Make sure the following script is included in your <head> of that document:

    # /becoming_a_django_entdev/chapter_5/templates/chapter_5/base/base_template_1.html

    ...

    <html lang="en" xmlns="http://www.w3.org/1999/xhtml">

        <head>

            ...

            <script defer type="text/javascript" src="{{ base_url }}{% static 'chapter_5/js/site-js.js' %}"></script>

        </head>

        ...

    </html>

This will load a single JavaScript file that is used to make a form interactive and lets the user add another instance of the inline ProspectiveBuyerFormset formset.

  1. If you didn't already copy the JavaScript files when copying the base_template_1.html file earlier in preparation for a previous exercise, then go ahead and create the /chapter_5/static/chapter_5/ and /chapter_5/static/chapter_5/js/ folders and the site-js.js file using your IDE, File Explorer, or the following commands:

    (virtual_env) PS > mkdir becoming_a_django_entdev/chapter_5/static/chapter_5

    (virtual_env) PS > mkdir becoming_a_django_entdev/chapter_5/static/chapter_5/js

    (virtual_env) PS > cd becoming_a_django_entdev/chapter_5/static/chapter_5/js

    (virtual_env) PS > touch site-js.js

  2. Inside your /chapter_5/js/site-js.js file, include the following variables:

    # /becoming_a_django_entdev/chapter_5/static/chapter_5/js/site-js.js

    let formsetContainer = document.querySelectorAll(

            '.formset-container'

        ),

        form = document.querySelector('#form'),

        addFormsetButton = document.querySelector(

            '#add-formset'

        ),

        totalForms = document.querySelector(

            '#id_form-TOTAL_FORMS'

        ),

        formsetNum = formsetContainer.length - 1;

  3. Add the following event listener to that same JavaScript file:

    # /becoming_a_django_entdev/chapter_5/static/chapter_5/js/site-js.js

    ...

    addFormsetButton.addEventListener(

        'click',

        $addFormset

    );

  4. Add the following function to that same JavaScript file:

    # /becoming_a_django_entdev/chapter_5/static/chapter_5/js/site-js.js

    ...

    function $addFormset(e) {

        e.preventDefault();

        let newForm = formsetContainer[0].cloneNode(true),

            formRegex = RegExp(`form-(\d){1}-`,'g');

        formsetNum++

        newForm.innerHTML = newForm.innerHTML.replace(

            formRegex,

            'form-${formsetNum}-'

        );

        form.insertBefore(newForm, addFormsetButton);

        totalForms.setAttribute(

            'value',

            '${formsetNum + 1}'

        );

    }

What we are doing is adding an event listener that listens for the click event of the Add Another Prospective Buyer button. When clicked, it will clone a single instance of <div class="formset-container"></div> and insert that cloned node just before the <button id="add-formset"></button> node. Since Django also requires the precise management of form data, we need to be sure that we update the relevant data every time an inline formset is added or removed. This is why we are finding the number of inline formsets that exist before we perform the clone action as the formsetNum variable. Then, we increment this number, which starts at index 0, using a regular expression method to search all the inner HTML nodes of the node with the formset-container CSS class. That incremented number is used to update all node attributes to the proper index of the new node that we inserted. We also update the value of the form object with id="id_form-TOTAL_FORMS" to the new total of inline formsets that exist.

If successful, when we click the Add Another Prospective Buyer button, we should see additional inline formsets added, just like the following:

Figure 5.8 – VehicleForm adding another inline formset

Figure 5.8 – VehicleForm adding another inline formset

Summary

By now, we have completed two major forms, one to act as the contact form and another to handle the vehicle object, created in Chapter 3, Models, Relations, and Inheritance. We added a variety of fields and discussed the differences between those field types. Using the email example over and over again as we did, we witnessed how validation works in many different ways. Depending on the requirements gathered for a project, we can then decide on several different writing patterns to align with those requirements. For example, if we wanted to completely eliminate the need for JavaScript validation, such as using my favorite library jQuery Validate, we could just write clean methods in form classes to perform all of the validation on the backend. This would use the power of Django to serve up the error messages. However, if we did use JavaScript-based validation on the frontend, we could write fields that create the node attributes for us, such as the <input> field attribute of required="", which would prevent a form from submitting if it had no value.

No matter the requirements of a project, we also discovered a really easy way to create our own field classes. Custom field classes let us preformat fields that support a Don't Repeat Yourself (DRY) style of writing. We explored the differences in view classes, form classes, and field classes, and then discussed ways to render those forms in a template.

In the next chapter, we will explore a user interface specifically tailored for the rapid development of forms that lets users update, create, and delete model objects on their own. This is called the Django admin site, which is basically a glorified way to render forms related to model management.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.220.21