Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Django Form Components

Tech 1

Intorduction

Previously, after submitting a form, custom validation rules had to be defined manually on both the frontend and backend. This led to repetitive work and was not concise. Django provides a built-in form component that encapsulates form validation, known as the Form component.

Main features of the Form component:

  1. Generate renderable HTML tags
  2. Validate user-submitted data
  3. Return validation error messages to the frontend
  4. Preserve previously entered data

Basic Validation Using Form Component

Template File (direct copy)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="/static/bootstrap3/css/bootstrap.min.css" />
    <script src="/static/bootstrap3/js/bootstrap.min.js"></script>
</head>
<body>
    <form action="" method="post">
        <h1>User Login</h1>
        <div>
            <label for="user">Username</label>
            <p><input type="text" name="name" id="user"></p>
        </div>
        <div>
            <label for="pwd">Password</label>
            <p><input type="password" name="pwd" id="pwd"></p>
        </div>
        <div>
            <label for="email">Email</label>
            <p><input type="text" name="email" id="email"></p>
        </div>
        <input type="submit">
    </form>
</body>
</html>

View File (view.py)

from django import forms
from django.shortcuts import render, HttpResponse

class UserForm(forms.Form):
    """Define login form validation rules"""
    name = forms.CharField(max_length=8)
    pwd = forms.CharField(max_length=8, min_length=3)
    email = forms.EmailField()

def login(request):
    if request.method == "GET":
        return render(request, "login.html")
    else:
        name = request.POST.get("name")
        pwd = request.POST.get("pwd")
        email = request.POST.get("email")
        # Validation
        # The keys in the dict must match the attribute names of UserForm
        form_obj = UserForm({'name': name, 'pwd': pwd, "email":email})
        # Alternatively: form_obj = UserForm(request.POST)
        if form_obj.is_valid():
            prompt = "Login successful"
        else:
            prompt = "Login failed, invalid format"
        return HttpResponse(prompt)

Methods and Properties of Form Component

# Example:
from django import forms

class RegForm(forms.Form):
    name = forms.CharField(max_length=6)
    pwd = forms.CharField(max_length=8, min_length=3)
    email = forms.EmailField()

# Passing values:
res = RegForm({'name': 'abcdef', 'pwd': '12', 'email': '123'})
# Requirements:
#     1. The keys in the dict must match the attribute names of RegForm
#     2. Must be a dictionary

# Check if validation passed
res.is_valid()    # Returns True or False

# Get error messages
res.errors
# Different versions may have different formats,
# but they can be considered as a dict of lists; access via dict.get()
res.errors.get('pwd')
# Returns: <ul class="errorlist"><li>Ensure this value has at least 3 characters (it has 2).</li></ul>

# Add custom error message
res.add_error('name', 'Must be uppercase')
print(res.errors.get('name'))
# Returns: <ul class="errorlist"><li>Must be uppercase</li></ul>

# Get cleaned data (only after calling errors or is_valid())
res.cleaned_data
# Returns: {'name': 'sun'}

# Get all data
res.data

# Extra fields are ignored, missing fields cause validation failure

Frontend Rendering with Form Component

The backend must pass the form object to the front end.

form_obj = UserForm()
return render(request, "reg.html", {"form_obj": form_obj})

Three ways to render in the template:

<!-- First way: as paragraphs -->
{{ form_obj.as_p }}
<!-- as unordered list -->
{{ form_obj.as_ul }}

<!-- Second way: manual rendering -->
<form>
    <p><label>{{ form_obj.name.label }}</label>{{ form_obj.name }}</p>
    <p><label>{{ form_obj.pwd.label }}</label>{{ form_obj.pwd }}</p>
</form>
<!-- Set label via parameter label="Username: " -->

<!-- Third way: iterate over fields -->
<form>
    {% for field in form_obj %}
        <p>
            <label>{{ field.label }}</label>
            {{ field }}
        </p>
    {% endfor %}
</form>

<!-- Add novalidate to disable browser validation -->
<form novalidate>

Form Validation and Error Message Rendering

Backend Implementation

from django import forms
from django.forms import widgets

class UserForm(forms.Form):
    name = forms.CharField(
        max_length=8,
        error_messages={
            "max_length": "Username maximum length is 8 characters",
            "required": "Username is required",
        }
    )
    pwd = forms.CharField(
        max_length=8,
        min_length=3,
        error_messages={
            "max_length": "Password maximum length is 8 characters",
            "min_length": "Password minimum length is 3 characters",
            "required": "Password is required",
        },
        widget=widgets.PasswordInput(attrs={"class": "form-control"}),
    )
    email = forms.EmailField(
        error_messages={
            "required": "Email is required",
            "invalid": "Invalid email format",
        }
    )

def login(request):
    if request.method == "GET":
        form_obj = UserForm()
        return render(request, "login.html", {"form_obj": form_obj})
    else:
        form_obj = UserForm(request.POST)
        if form_obj.is_valid():
            return HttpResponse("OK")
        else:
            return render(request, "login.html", {"form_obj": form_obj})

Front end Template

<body>
    <div>
        <form method="post" action="" novalidate>
            {% for field in form_obj %}
                <p>
                    <label>{{ field.label }}</label>
                    {{ field }}{{ field.errors.0 }}
                </p>
            {% endfor %}
            <input type="submit" value="Submit">
        </form>
    </div>
</body>

Hook Functions (Custom Validation Rules)

Local Hook (per field)

from django.core.exceptions import ValidationError

def clean_name(self):
    name = self.cleaned_data.get("name")
    if "aaa" in name:
        # Add error via add_error
        self.add_error("name",  "Cannot contain 'aaa'")
        # Alternatively
        # raise ValidationError("Cannot contain 'aaa'")
    # Must return the cleaned value
    return name

Global Hook (form-wide)

def clean(self):
    pwd_value = self.cleaned_data.get("pwd")
    re_pwd_value = self.cleaned_data.get("re_pwd")
    if pwd_value != re_pwd_value:
        # Add error
        self.add_error("re_pwd",  "Passwords do not match")
        # Or raise ValidationError with dict
        # raise ValidationError({"re_pwd": "Passwords do not match"})
    # Return cleaned data (optional)
    return self.cleaned_data

Important notes:

  • In local hooks, using raise prevents the field from appearing in cleaned_data, while add_error does not.
  • ValidationError can take a string, list, or dict. For local hooks, use string or list. For global hooks, dict is recommended (or add_error).

Common Parameters in Form Fields

Field Parameters (inherited by all field types)

Parameter Description
required required=False allows empty value
widget Sets the HTML input type
initial initial="value" sets default value
label label="Name" sets label text
label_suffix label_suffix=">>" adds suffix to label
error_messages Custom error messages dict
help_text Help text displayed next to field
validators=[] Custom regex validators
localize=False Localization support
disabled=False If True, field is read-only
# validators example:
from django.core.validators import RegexValidator

name = forms.CharField(
    min_length=2,
    validators=[RegexValidator(regex='^\d+$', message="Must be digits")],
    error_messages={
        'max_length': 'Must be less than 3 characters',
        'min_length': 'Must be more than 2 characters',
    },
)

Widget Usage for Radio, Checkbox, Select

Radio Buttons (Single Choice)

from django import forms
from django.forms import widgets

# Approach 1:
gender = forms.ChoiceField(
    choices=((1, "Male"), (2, "Female"), (3, "Other")),
    widget=widgets.RadioSelect()
)

# Approach 2:
gender = forms.IntegerField(
    widget=widgets.RadioSelect(
        choices=((1, "Male"), (2, "Female"), (3, "Other")),
    )
)

Generated HTML:

<div>
    <label for="id_gender_0">Gender:</label>
    <ul id="id_gender" class="radio">
        <li><label for="id_gender_0"><input type="radio" name="gender" value="1" class="radio" required="" id="id_gender_0"> Male</label></li>
        <li><label for="id_gender_1"><input type="radio" name="gender" value="2" class="radio" required="" id="id_gender_1"> Female</label></li>
        <li><label for="id_gender_2"><input type="radio" name="gender" value="3" class="radio" required="" id="id_gender_2"> Other</label></li>
    </ul>
</div>

Checkbox (Single)

keep = forms.BooleanField(
    label="Remember password",
    initial=True,
    widget=forms.widgets.CheckboxInput()
)

Generated HTML:

<p>
    <label for="id_keep">Remember password:</label>
    <input type="checkbox" name="keep" id="id_keep" checked>
</p>

Checkbox (Multiple)

hobby = forms.MultipleChoiceField(
    choices=((1, "Basketball"), (2, "Football"), (3, "Ping Pong")),
    widget=forms.widgets.CheckboxSelectMultiple()
)

# Or
hobby = forms.CharField(
    widget=forms.widgets.CheckboxSelectMultiple(
        choices=((1, "Basketball"), (2, "Football"), (3, "Ping Pong")),
    )
)

Generated HTML:

<div>
    <label>Hobby:</label>
    <ul id="id_hobby">
        <li><label for="id_hobby_0"><input type="checkbox" name="hobby" value="1" id="id_hobby_0"> Basketball</label></li>
        <li><label for="id_hobby_1"><input type="checkbox" name="hobby" value="2" id="id_hobby_1"> Football</label></li>
        <li><label for="id_hobby_2"><input type="checkbox" name="hobby" value="3" id="id_hobby_2"> Ping Pong</label></li>
    </ul>
</div>

Select (Single)

hobby = forms.ChoiceField(
    choices=((1, "Basketball"), (2, "Football"), (3, "Ping Pong")),
    widget=forms.widgets.Select()
)

# Or
hobby = forms.CharField(
    widget=forms.widgets.Select(
        choices=((1, "Basketball"), (2, "Football"), (3, "Ping Pong")),
    )
)

Generated HTML:

<div>
    <label for="id_hobby">Hobby:</label>
    <select name="hobby" id="id_hobby">
        <option value="1">Basketball</option>
        <option value="2">Football</option>
        <option value="3">Ping Pong</option>
    </select>
</div>

Select (Multiple)

hobby = forms.MultipleChoiceField(
    choices=((1, "Basketball"), (2, "Football"), (3, "Ping Pong")),
    widget=forms.widgets.SelectMultiple()
)

# Or
hobby = forms.CharField(
    widget=forms.widgets.SelectMultiple(
        choices=((1, "Basketball"), (2, "Football"), (3, "Ping Pong")),
    )
)

Dynamic Choices from Database

To update choices in real time, override __init__:

from django import forms
from django.forms import widgets
from . import models

class RegisterForm(forms.Form):
    hobby = forms.MultipleChoiceField(
        widget=forms.widgets.SelectMultiple()
    )

    def __init__(self, *args, **kwargs):
        super(RegisterForm, self).__init__(*args, **kwargs)
        self.fields['hobby'].choices = models.Hobby.objects.values_list("id", "name")

Built-in Field Types (Similar to Model Fields)

Field
    required=True,               # Whether empty is allowed
    widget=None,                 # HTML plugin
    label=None,                  # Label text or content
    initial=None,                # Initial value
    help_text='',                # Help text
    error_messages=None,         # Error messages dict
    validators=[],               # Custom validators
    localize=False,              # Localization support
    disabled=False,              # Read-only
    label_suffix=None            # Label suffix

CharField(Field)
    max_length=None,             # Maximum length
    min_length=None,             # Minimum length
    strip=True                   # Remove whitespace

IntegerField(Field)
    max_value=None,              # Maximum value
    min_value=None,              # Minimum value

FloatField(IntegerField)
    ...

DecimalField(IntegerField)
    max_value=None,
    min_value=None,
    max_digits=None,             # Total digits
    decimal_places=None,         # Decimal places

BaseTemporalField(Field)
    input_formats=None           # Date/time formats
DateField(BaseTemporalField)     # Format: 2015-09-01
TimeField(BaseTemporalField)     # Format: 11:12
DateTimeField(BaseTemporalField) # Format: 2015-09-01 11:12

DurationField(Field)             # Time interval: %d %H:%M:%S.%f

RegexField(CharField)
    regex,                       # Custom regex
    max_length=None,
    min_length=None,
    error_message=None,          # Use error_messages dict for 'invalid'

EmailField(CharField)
    ...

FileField(Field)
    allow_empty_file=False       # Whether empty file allowed

ImageField(FileField)
    ...  # Requires Pillow: pip install Pillow
    # Note: enctype="multipart/form-data" in form, and request.FILES in view

URLField(Field)
    ...

BooleanField(Field)
    ...

NullBooleanField(BooleanField)
    ...

ChoiceField(Field)
    choices=(),                  # Options: ((0,'Shanghai'),(1,'Beijing'))
    required=True,
    widget=None,                 # Default select
    label=None,
    initial=None,
    help_text='',

ModelChoiceField(ChoiceField)
    queryset,                    # Query database
    empty_label="---------",
    to_field_name=None,          # Field for value
    limit_choices_to=None        # Additional filtering for ModelForm

ModelMultipleChoiceField(ModelChoiceField)
    ...

TypedChoiceField(ChoiceField)
    coerce = lambda val: val     # Convert selected value
    empty_value= ''              # Default for empty

MultipleChoiceField(ChoiceField)
    ...

TypedMultipleChoiceField(MultipleChoiceField)
    coerce = lambda val: val
    empty_value= ''

ComboField(Field)
    fields=()                    # Combine validations: e.g., fields=[CharField(max_length=20), EmailField()]

MultiValueField(Field)
    # Abstract class; use with MultiWidget

SplitDateTimeField(MultiValueField)
    input_date_formats=None,     # Date formats list
    input_time_formats=None      # Time formats list

FilePathField(ChoiceField)       # File path selection
    path,                        # Folder path
    match=None,                  # Regex match
    recursive=False,             # Include subfolders
    allow_files=True,
    allow_folders=False,
    required=True,
    widget=None,
    label=None,
    initial=None,
    help_text='',

GenericIPAddressField
    protocol='both',             # both, ipv4, ipv6
    unpack_ipv4=False            # Unpack IPv4 address

SlugField(CharField)            # Letters, digits, underscore, hyphen
UUIDField(CharField)            # UUID type

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.