Bài 12: Django Forms

Django Forms Là Gì?

Django Forms là framework giúp tạo, validate và xử lý HTML forms một cách dễ dàng và an toàn.

# Without Django Forms (manual HTML)<form method="post">    <input type="text" name="username">    <input type="email" name="email">    <input type="password" name="password">    <button type="submit">Submit</button></form> # With Django Formsfrom django import forms class UserForm(forms.Form):    username = forms.CharField(max_length=100)    email = forms.EmailField()    password = forms.CharField(widget=forms.PasswordInput) # In view:form = UserForm() # In template:{{ form.as_p }} # Django automatically:# - Generates HTML# - Validates data# - Handles errors# - Provides CSRF protection# - Cleans and normalizes data

Tại Sao Dùng Django Forms?

# Benefits:# 1. Automatic HTML generation# 2. Data validation# 3. Error handling# 4. Security (CSRF protection)# 5. Data cleaning and normalization# 6. Reusable form logic# 7. Integration with models (ModelForm) # Example: Validationform = UserForm(request.POST)if form.is_valid():    # Data is validated and cleaned    username = form.cleaned_data['username']    email = form.cleaned_data['email']else:    # Form has errors    print(form.errors)

Creating Basic Forms

Simple Form

# forms.pyfrom django import forms class ContactForm(forms.Form):    name = forms.CharField(        max_length=100,        required=True,        label='Your Name'    )    email = forms.EmailField(        required=True,        label='Your Email'    )    subject = forms.CharField(        max_length=200,        required=True    )    message = forms.CharField(        widget=forms.Textarea,        required=True,        help_text='Enter your message here'    ) # views.pyfrom django.shortcuts import renderfrom .forms import ContactForm def contact_view(request):    if request.method == 'POST':        form = ContactForm(request.POST)        if form.is_valid():            # Process form data            name = form.cleaned_data['name']            email = form.cleaned_data['email']            subject = form.cleaned_data['subject']            message = form.cleaned_data['message']                        # Send email, save to database, etc.            return render(request, 'success.html')    else:        form = ContactForm()        return render(request, 'contact.html', {'form': form})

Form with Initial Data

# Provide initial valuesform = ContactForm(initial={    'name': 'John Doe',    'email': '[email protected]'}) # From request.userform = ContactForm(initial={    'name': request.user.get_full_name(),    'email': request.user.email}) # Dynamic initial dataclass ProfileForm(forms.Form):    username = forms.CharField(max_length=100)    bio = forms.CharField(widget=forms.Textarea)        def __init__(self, *args, **kwargs):        user = kwargs.pop('user', None)        super().__init__(*args, **kwargs)                if user:            self.initial['username'] = user.username            self.initial['bio'] = user.profile.bio

Form with Choices

# Static choicesclass FeedbackForm(forms.Form):    RATING_CHOICES = [        (1, 'Very Bad'),        (2, 'Bad'),        (3, 'Average'),        (4, 'Good'),        (5, 'Excellent'),    ]        rating = forms.ChoiceField(        choices=RATING_CHOICES,        widget=forms.RadioSelect    )        CATEGORY_CHOICES = [        ('bug', 'Bug Report'),        ('feature', 'Feature Request'),        ('question', 'Question'),        ('other', 'Other'),    ]        category = forms.ChoiceField(choices=CATEGORY_CHOICES)    feedback = forms.CharField(widget=forms.Textarea) # Dynamic choices from databaseclass PostForm(forms.Form):    title = forms.CharField(max_length=200)    category = forms.ModelChoiceField(        queryset=Category.objects.all(),        empty_label="Select a category"    )    tags = forms.ModelMultipleChoiceField(        queryset=Tag.objects.all(),        widget=forms.CheckboxSelectMultiple,        required=False    )

Form Fields

Common Field Types

from django import forms class ExampleForm(forms.Form):    # Text fields    char_field = forms.CharField(max_length=100)    text_field = forms.CharField(widget=forms.Textarea)    email_field = forms.EmailField()    url_field = forms.URLField()    slug_field = forms.SlugField()        # Numeric fields    integer_field = forms.IntegerField()    float_field = forms.FloatField()    decimal_field = forms.DecimalField(max_digits=10, decimal_places=2)        # Boolean field    boolean_field = forms.BooleanField()    null_boolean_field = forms.NullBooleanField()  # True/False/None        # Choice fields    choice_field = forms.ChoiceField(choices=[('a', 'A'), ('b', 'B')])    multiple_choice = forms.MultipleChoiceField(choices=[...])        # Date/Time fields    date_field = forms.DateField()    time_field = forms.TimeField()    datetime_field = forms.DateTimeField()        # File fields    file_field = forms.FileField()    image_field = forms.ImageField()        # Other fields    uuid_field = forms.UUIDField()    json_field = forms.JSONField()    regex_field = forms.RegexField(regex=r'^\d{3}-\d{3}-\d{4}$')

Field Arguments

class DetailedForm(forms.Form):    # Required/Optional    required_field = forms.CharField(required=True)    optional_field = forms.CharField(required=False)        # Labels and help text    labeled_field = forms.CharField(        label='Custom Label',        help_text='Enter your custom text here'    )        # Initial values    initial_field = forms.CharField(initial='Default value')        # Max/Min length    length_field = forms.CharField(        min_length=5,        max_length=100    )        # Max/Min value (for numbers)    number_field = forms.IntegerField(        min_value=1,        max_value=100    )        # Validators    validated_field = forms.CharField(        validators=[validate_no_spaces, validate_uppercase]    )        # Error messages    custom_error = forms.CharField(        error_messages={            'required': 'This field is mandatory!',            'max_length': 'Too long! Maximum 100 characters.',        }    )        # Disabled field    disabled_field = forms.CharField(        disabled=True,        initial='Cannot edit this'    )        # Widget attributes    styled_field = forms.CharField(        widget=forms.TextInput(attrs={            'class': 'form-control',            'placeholder': 'Enter text',            'id': 'custom-id'        })    )

Custom Field Validators

from django.core.exceptions import ValidationError # Custom validator functiondef validate_uppercase(value):    if not value.isupper():        raise ValidationError(            'Value must be uppercase',            code='not_uppercase'        ) def validate_no_spaces(value):    if ' ' in value:        raise ValidationError('No spaces allowed') def validate_even_number(value):    if value % 2 != 0:        raise ValidationError('Must be an even number') # Use in formclass ValidatedForm(forms.Form):    code = forms.CharField(        validators=[validate_uppercase, validate_no_spaces]    )    count = forms.IntegerField(        validators=[validate_even_number]    )        # Validator with parameters    def validate_min_words(min_words):        def validator(value):            word_count = len(value.split())            if word_count < min_words:                raise ValidationError(                    f'Must contain at least {min_words} words'                )        return validator        description = forms.CharField(        widget=forms.Textarea,        validators=[validate_min_words(10)]    )

Form Validation

Field-Level Validation

class UserRegistrationForm(forms.Form):    username = forms.CharField(max_length=100)    email = forms.EmailField()    password = forms.CharField(widget=forms.PasswordInput)    confirm_password = forms.CharField(widget=forms.PasswordInput)    age = forms.IntegerField()        # Field-level validation: clean_<fieldname>()    def clean_username(self):        username = self.cleaned_data['username']                # Check if username already exists        if User.objects.filter(username=username).exists():            raise ValidationError('Username already taken')                # Check if username contains only alphanumeric        if not username.isalnum():            raise ValidationError('Username must be alphanumeric')                # Must return cleaned value        return username        def clean_email(self):        email = self.cleaned_data['email']                # Check if email already registered        if User.objects.filter(email=email).exists():            raise ValidationError('Email already registered')                # Normalize email        return email.lower()        def clean_age(self):        age = self.cleaned_data['age']                # Must be 18 or older        if age < 18:            raise ValidationError('Must be 18 or older to register')                return age

Form-Level Validation

class RegistrationForm(forms.Form):    username = forms.CharField(max_length=100)    email = forms.EmailField()    password = forms.CharField(widget=forms.PasswordInput)    confirm_password = forms.CharField(widget=forms.PasswordInput)        # Form-level validation: clean()    def clean(self):        cleaned_data = super().clean()        password = cleaned_data.get('password')        confirm_password = cleaned_data.get('confirm_password')                # Compare two fields        if password and confirm_password:            if password != confirm_password:                raise ValidationError('Passwords do not match')                return cleaned_data        # Alternative: Add error to specific field    def clean(self):        cleaned_data = super().clean()        password = cleaned_data.get('password')        confirm_password = cleaned_data.get('confirm_password')                if password != confirm_password:            self.add_error('confirm_password', 'Passwords do not match')                return cleaned_data

Complex Validation

class BookingForm(forms.Form):    check_in = forms.DateField()    check_out = forms.DateField()    guests = forms.IntegerField(min_value=1, max_value=10)    room_type = forms.ChoiceField(choices=[...])        def clean_check_in(self):        check_in = self.cleaned_data['check_in']        today = timezone.now().date()                # Cannot book past dates        if check_in < today:            raise ValidationError('Check-in date cannot be in the past')                # Cannot book more than 1 year in advance        max_date = today + timezone.timedelta(days=365)        if check_in > max_date:            raise ValidationError('Cannot book more than 1 year in advance')                return check_in        def clean(self):        cleaned_data = super().clean()        check_in = cleaned_data.get('check_in')        check_out = cleaned_data.get('check_out')        guests = cleaned_data.get('guests')        room_type = cleaned_data.get('room_type')                # Check-out must be after check-in        if check_in and check_out:            if check_out <= check_in:                raise ValidationError('Check-out must be after check-in')                        # Minimum stay of 1 night            nights = (check_out - check_in).days            if nights < 1:                raise ValidationError('Minimum stay is 1 night')                        # Maximum stay of 30 nights            if nights > 30:                raise ValidationError('Maximum stay is 30 nights')                # Check room availability        if all([check_in, check_out, room_type]):            available = check_room_availability(                room_type, check_in, check_out, guests            )            if not available:                raise ValidationError(                    f'No {room_type} rooms available for selected dates'                )                return cleaned_data

Form Rendering

Rendering Methods

# Template: contact.html<form method="post">    {% csrf_token %}        {# Method 1: Automatic rendering #}    {{ form.as_p }}      <!-- Each field in <p> tag -->    {{ form.as_table }}  <!-- Each field in <tr> tag -->    {{ form.as_ul }}     <!-- Each field in <li> tag -->    {{ form.as_div }}    <!-- Each field in <div> tag (Django 4.1+) -->        <button type="submit">Submit</button></form> {# Output of form.as_p: #}<form method="post">    <input type="hidden" name="csrfmiddlewaretoken" value="...">        <p>        <label for="id_name">Your Name:</label>        <input type="text" name="name" maxlength="100" required id="id_name">    </p>        <p>        <label for="id_email">Your Email:</label>        <input type="email" name="email" required id="id_email">    </p>        <button type="submit">Submit</button></form>

Manual Field Rendering

# Template: Manual rendering<form method="post">    {% csrf_token %}        {# Render individual fields #}    <div class="form-group">        {{ form.name.label_tag }}        {{ form.name }}        {% if form.name.errors %}            <div class="error">{{ form.name.errors }}</div>        {% endif %}        {% if form.name.help_text %}            <small>{{ form.name.help_text }}</small>        {% endif %}    </div>        <div class="form-group">        {{ form.email.label_tag }}        {{ form.email }}        {{ form.email.errors }}    </div>        <button type="submit">Submit</button></form> {# Loop through all fields #}<form method="post">    {% csrf_token %}        {% for field in form %}        <div class="form-group">            {{ field.label_tag }}            {{ field }}            {% if field.errors %}                <div class="error">{{ field.errors }}</div>            {% endif %}            {% if field.help_text %}                <small>{{ field.help_text }}</small>            {% endif %}        </div>    {% endfor %}        <button type="submit">Submit</button></form>

Styled Forms

# Bootstrap-styled form<form method="post" class="needs-validation" novalidate>    {% csrf_token %}        {% for field in form %}        <div class="mb-3">            <label for="{{ field.id_for_label }}" class="form-label">                {{ field.label }}                {% if field.field.required %}                    <span class="text-danger">*</span>                {% endif %}            </label>                        {% if field.field.widget.input_type == 'checkbox' %}                <div class="form-check">                    {{ field }}                    <label class="form-check-label" for="{{ field.id_for_label }}">                        {{ field.label }}                    </label>                </div>            {% else %}                {{ field }}            {% endif %}                        {% if field.errors %}                <div class="invalid-feedback d-block">                    {{ field.errors }}                </div>            {% endif %}                        {% if field.help_text %}                <small class="form-text text-muted">                    {{ field.help_text }}                </small>            {% endif %}        </div>    {% endfor %}        <button type="submit" class="btn btn-primary">Submit</button></form>

Form Attributes

# Add CSS classes to form fieldsclass StyledForm(forms.Form):    def __init__(self, *args, **kwargs):        super().__init__(*args, **kwargs)                # Add class to all fields        for field_name, field in self.fields.items():            field.widget.attrs['class'] = 'form-control'                # Add placeholder        self.fields['email'].widget.attrs['placeholder'] = 'Enter email'                # Add custom attributes        self.fields['password'].widget.attrs.update({            'class': 'form-control',            'placeholder': 'Enter password',            'autocomplete': 'new-password'        }) # Or in field definitionclass ContactForm(forms.Form):    name = forms.CharField(        widget=forms.TextInput(attrs={            'class': 'form-control',            'placeholder': 'Your name'        })    )    email = forms.EmailField(        widget=forms.EmailInput(attrs={            'class': 'form-control',            'placeholder': '[email protected]'        })    )

Error Handling

Displaying Errors

# Template: Display all errors<form method="post">    {% csrf_token %}        {# Non-field errors (from clean()) #}    {% if form.non_field_errors %}        <div class="alert alert-danger">            {{ form.non_field_errors }}        </div>    {% endif %}        {# All form errors #}    {% if form.errors %}        <div class="alert alert-danger">            <strong>Please correct the following errors:</strong>            <ul>                {% for field in form %}                    {% for error in field.errors %}                        <li>{{ field.label }}: {{ error }}</li>                    {% endfor %}                {% endfor %}                {% for error in form.non_field_errors %}                    <li>{{ error }}</li>                {% endfor %}            </ul>        </div>    {% endif %}        {# Render form fields #}    {{ form.as_p }}        <button type="submit">Submit</button></form>

Custom Error Messages

class CustomErrorForm(forms.Form):    username = forms.CharField(        max_length=50,        error_messages={            'required': 'Username is required!',            'max_length': 'Username too long! Max 50 characters.',        }    )        email = forms.EmailField(        error_messages={            'required': 'Please provide your email address.',            'invalid': 'Please enter a valid email address.',        }    )        age = forms.IntegerField(        min_value=18,        max_value=120,        error_messages={            'required': 'Age is required.',            'invalid': 'Please enter a valid number.',            'min_value': 'You must be at least 18 years old.',            'max_value': 'Please enter a realistic age.',        }    )

Programmatic Error Handling

# View: Handle form errorsdef register_view(request):    if request.method == 'POST':        form = RegistrationForm(request.POST)                if form.is_valid():            # Process valid form            user = form.save()            return redirect('success')        else:            # Form has errors            # Errors are automatically passed to template                        # Access errors in view            if 'email' in form.errors:                print("Email errors:", form.errors['email'])                        # Get all errors as dict            all_errors = form.errors.as_data()                        # Get errors as JSON            json_errors = form.errors.as_json()    else:        form = RegistrationForm()        return render(request, 'register.html', {'form': form}) # Add custom error in viewdef custom_validation_view(request):    form = SomeForm(request.POST)        if form.is_valid():        # Additional validation        if some_external_check_fails():            form.add_error('field_name', 'Custom error message')            form.add_error(None, 'Non-field error')  # General error        return render(request, 'template.html', {'form': form})

Bài Tập

Bài 1: Basic Form

Task: Tạo form đăng ký newsletter:

# Tạo NewsletterForm với:# - email (EmailField, required)# - frequency (ChoiceField: daily, weekly, monthly)# - topics (MultipleChoiceField: tech, business, sports, entertainment)# - agree_terms (BooleanField) # Validate:# - Email không được trùng trong database# - Must agree to terms# - At least one topic selected # View:# - GET: Show empty form# - POST: Validate và lưu subscription# - Show success message

Bài 2: Complex Validation

Task: Tạo form đặt tour du lịch:

# TourBookingForm:# - destination (ChoiceField)# - departure_date (DateField)# - return_date (DateField)# - adults (IntegerField, 1-10)# - children (IntegerField, 0-5)# - accommodation (ChoiceField: hotel, hostel, none) # Validation rules:# - Departure date must be at least 7 days from today# - Return date must be after departure date# - Tour duration between 3-30 days# - At least 1 adult required# - Children < 2 years old travel free (add age field)# - Check tour availability for selected dates

Bài 3: Dynamic Form

Task: Tạo form với dynamic fields:

# ProductOrderForm:# - product (ModelChoiceField)# - quantity (IntegerField)# - If product requires customization:#   - Show additional fields (color, size, engraving_text)# - Calculate total price dynamically# - Show shipping options based on product weight # Hints:# - Override __init__ to add conditional fields# - Use clean() to validate combinations# - Consider product stock availability

Bài 4: Multi-Step Form

Task: Tạo multi-step registration form:

# Step 1: Account Information# - username, email, password, confirm_password # Step 2: Personal Information# - first_name, last_name, birth_date, phone # Step 3: Preferences# - language, timezone, newsletter, notifications # Requirements:# - Store data in session between steps# - Validate each step before proceeding# - Allow going back to edit previous steps# - Final step saves everything to database

Tài Liệu Tham Khảo


Previous: Bài 11: CRUD Operations | Next: Bài 13: Model Forms