Forms

What are Django Forms?

Django's forms handle HTML form rendering, validation, and data processing. You can create two types.

  • Forms - manually define fields (for custom inputs).
  • ModelForms - automatically generate a form from a model.

Django forms integrate with views and templates to simplify capturing user input, validating it, and saving it to the database.

Utilising Forms

Creating the Form Class

  • For a form based on a model, in the forms.py file of your app, import forms from django along with your model from .models
  • Create a form class inheriting from forms.ModelForm
  • Add a child class of Meta that defines the model that you are using and fields that will be available in the form. ModelForm creates fields automatically based on your model
  • The Meta class in where custom widgets, labels, help_texts or excluded fields are defined
from django import forms
# Import your model to make a form from
from .models import ModelName

class FormName(forms.ModelForm):
    class Meta:
        model = ModelName
        fields = ['model_field_1', ' model_field_2']

To create a form not based on a model you can use:


from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(max_length=100, label="Your Name")
    email = forms.EmailField(label="Your Email")
    message = forms.CharField(widget=forms.Textarea, label="Your Message")

Adding Extra Fields

To add an extra field to your form that is not in your model or you are excluding from your fields variable, you can define it above the Meta class. By default these extra fields will appear in your form after those defined in the fields variable.

class EditForm(ModelForm):
    '''Form to edit'''

    # Extra field not in the model
    unavailability_input = forms.CharField(
        widget=forms.Textarea(
            attrs={
                'placeholder': 'Enter dates as YYYY-MM-DD, separated by commas'
            }
        ),
        required=False,
    )

Add the Form to your View

Now that the form is defined, it can be imported in your views.py file.

# Imports from the forms.py file in the same app
from .forms import FormName

Then a view can be created or updated to use the form. The key points are:

  • The view will assume that the request method is GET by default. To handle form submission we will need to split the function into and if check if the request method is POST and 'else'. Everything already in the function should go into the 'else' part such as defined variables.
  • Define the form variable and add it to both the POST section with the information from the submitted form and in the else section in order to display the form on the page. Add the form to the context so that is rendered.
  • Validate the form fields against their types, requirements, and validation rules using form.is_valid() and if it is, then save it before redirecting the user.
from .forms import FormName
    
def add_item(request):
	# Add the if to check if the request method is POST
    if request.method == 'POST':
	    # Define the form using the information submitted
        form = FormName(request.POST)
	    # Check that the form is valid, then save it and redirect         
        if form.is_valid():
            form.save()
            return redirect('view_function_2')  # Redirect after saving to another view

	# For the case of GET (e.g. page loading)
    else:
	# Define the form in order to acces it with templating language
        form = FormName()

    # Add the form to the context, form is defined in POST and GET so in either option there will be a form variable to add
    context = {'form': form}
    return render(request, 'todo/add_item.html', context)

Add the Form to your Template

  • Add the form to your template with a method of POST and the action to be the url associated with your above defined view function.
  • It is imported to include as the first thing inside your form for security reasons. Django will not process the form if this is missing.
  • Render the form using the variable defined within your context. You can change the layout of your form using:
    • {{ form.as_p }}
    • {{ form.as_table }}
    • {{ form.as_ul }}
  • Add in a submit button to your form.
<form method="POST" action="{% url 'add' %}">
    {% csrf_token %}
    {{ form.as_p }}  <!-- Renders the form fields with <p> element wrappers -->
    <button type="submit">Submit</button>
</form>

Widgets

Widgets in Django forms control how a field is rendered in HTML by describing the element (<input>, <textarea> etc.), and attributes such as class, id, or placeholder. For ModelForms, the widgets are defined as a dictionary inside the Meta class using the field name as the key and the element and attribute options as the value.

# Add widgets in the Meta Class
class Meta:
    model = Item
    fields = ['name']
    widgets = {
    'name': forms.TextInput(attrs={'class': 'form-control'}),
}

In a form not based on a ModelForm you would add the widgets in the view.

from django import forms

class MyForm(forms.Form):
    name = forms.CharField(
        widget=forms.TextInput(attrs={
            'class': 'form-control',
            'placeholder': 'Enter your name',
            'id': 'name-input'
        })
    )

Here is a quick reference of common built-in widgets.

Text Input Widgets

  • forms.TextInput → <input type="text">
  • forms.EmailInput → <input type="email">
  • forms.URLInput → <input type="url">
  • forms.PasswordInput → <input type="password">
  • forms.HiddenInput → <input type="hidden">

Text Area

  • forms.Textarea → <textarea></textarea>

Numeric Inputs

  • forms.NumberInput → <input type="number">
  • forms.RangeInput → <input type="range">

Date and Time Inputs

  • forms.DateInput → <input type="date">
  • forms.DateTimeInput → <input type="datetime-local">
  • forms.TimeInput → <input type="time">

Choice Inputs

  • forms.Select → <select> (dropdown)
  • forms.SelectMultiple → <select multiple>
  • forms.RadioSelect → List of radio buttons
  • forms.CheckboxInput → Single checkbox
  • forms.CheckboxSelectMultiple → List of checkboxes
  • forms.NullBooleanSelect → Dropdown with "Unknown / Yes / No"

File Uploads

  • forms.FileInput → <input type="file">
  • forms.ClearableFileInput → File input with a "clear" checkbox (default for FileField/ImageField).

Common Attributes you can pass:

  • class: For CSS classes ('form-control')
  • id: For targeting with JavaScript or CSS
  • placeholder: Placeholder text
  • rows/cols: For Textarea
  • style: Inline CSS
  • type: Change the input type (e.g., text, email)
  • multiple: Allow multiple selections for Select
  • disabled: Disable the field
  • readonly: Make it read only

Validation

You can define custom validation by adding methods to your form. For best practice, these are defined within the form rather than the view, as it will help validate the input before submission.

For field specific validation, it is best to define a function that follows the syntax of clean_fieldname(self). This is where custom checks for specific fields are added. These will be run first when the form is submitted. If you want to check for length of entry, if it has a capital letter, if it is an email etc prior to submission, those checks are covered by the widgets.

def clean_name(self):
    data = self.cleaned_data['name']
    if 'bad' in data:
        raise forms.ValidationError("Invalid name.")
    return data

In the case of ModelForms you can also add validation into a clean(self) function at the bottom of the form.

# models.py
from django.core.exceptions import ValidationError

class Room(models.Model):
    price = models.DecimalField(max_digits=6, decimal_places=2)

    def clean(self):
        if self.price <= 0:
            raise ValidationError("Price must be positive.")

Summary

Step What to do Notes
HTML Form <form method="POST" action="{% url 'add' %}"></form> Use {% url 'name' %} to link to a named route
CSRF Token {% csrf_token %} Must be inside the <form> to avoid CSRF errors
Create Form class ItemForm(forms.ModelForm): Located in forms.py
Meta Class model = Item, fields = ['name', 'done'] Defines form structure
Add to View form = ItemForm(request.POST) Use request.POST on POST request
Validate & Save if form.is_valid(): form.save() Validates input and saves to DB
Pass to Template context = {'form': form} Included in render()
Use in Template {{ form.as_p }} Or use individual fields: {{ form.name }}
Submit Button <button type="submit">Save</button> Must be inside the <form></form>

Example Form

# Imports
from django.forms import ModelForm
from .models import Room
from django import forms
from datetime import datetime


class EditRoomForm(ModelForm):
    '''Form to edit a room.'''
    # Handle the unavailable dates differently
    unavailability_input = forms.CharField(
        widget=forms.Textarea(
        # Add placeholder text to instruct user
            attrs={
                'placeholder': 'Enter dates as YYYY-MM-DD, separated by commas'
            }
        ),
        # Make the field not required
        required=False,
    )

    class Meta:
        '''Meta class for the edit room form'''
        model = Room
        # Fields listed 
        fields = [
            'name', 'sanitised_name', 'amenities', 'description',
            'image', 'price'
        ]

    def clean_unavailability_input(self):
        '''Convert the input dates to usable format'''
        input_data = self.cleaned_data['unavailability_input']
        if not input_data:
            return []
        dates = [date.strip() for date in input_data.split(',')]
        # Validate date format
        for date in dates:
            try:
                # Convert to correct date format
                datetime.strptime(date, '%Y-%m-%d')
            except ValueError:
                raise forms.ValidationError(
                    f"Invalid date format: {date}. Use YYYY-MM-DD."
                )
        return dates

    def save(self, commit=True):
        '''Overwrite the save to include the cleaned unavailable dates'''
        room = super().save(commit=False)
        # Save the cleaned list of dates into the 'unavailability' model field
        room.unavailability = self.cleaned_data['unavailability_input']
        if commit:
            room.save()
        return room

Troubleshooting

If a form is not working, check that the first thing inside the form element in your HTML page is {% csrf_token %}.