Back to Curriculum

Django Forms and User Input

📚 Lesson 5 of 8 ⏱️ 50 min

Django Forms and User Input

50 min

Django forms handle user input validation, processing, and rendering. They provide a secure way to collect and validate user data. Forms automatically handle HTML generation, validation, error display, and data cleaning. This reduces boilerplate code and ensures consistent form handling across your application.

ModelForms automatically generate form fields based on your models, while regular forms give you complete control over field definitions. ModelForms are perfect for CRUD operations, automatically creating fields that match your model fields. Regular forms are better when you need custom validation or fields that don't map directly to models.

Form validation includes both client-side and server-side validation, with customizable error messages and field widgets. Django forms perform server-side validation by default, which is more secure than client-side only. The clean() and clean_<field>() methods allow you to add custom validation logic. Field widgets control how form fields are rendered in HTML.

Understanding CSRF protection, file uploads, and form security is crucial for building safe Django applications. Django automatically includes CSRF tokens in forms to prevent cross-site request forgery attacks. File uploads require special handling with enctype='multipart/form-data' and proper file storage configuration. Understanding these security features is essential for production applications.

Form sets allow you to manage multiple forms on a single page, useful for creating related objects. Inline formsets work with related models, enabling you to edit related objects from a parent form. These advanced features enable complex form interactions while maintaining Django's security and validation features.

Django forms integrate seamlessly with views and templates. In views, forms handle GET requests (displaying empty forms) and POST requests (processing submitted data). Templates use form methods like as_p(), as_table(), or manual rendering for complete control. Understanding this integration is key to building interactive Django applications.

Key Concepts

  • Django forms handle validation, processing, and rendering of user input.
  • ModelForms automatically generate fields from model definitions.
  • Form validation includes server-side validation with custom clean methods.
  • CSRF protection is automatically included in Django forms.
  • File uploads require special handling with multipart/form-data.

Learning Objectives

Master

  • Creating ModelForms and regular forms
  • Implementing custom form validation
  • Handling file uploads securely
  • Using formsets for multiple form management

Develop

  • Understanding form security best practices
  • Designing user-friendly form interfaces
  • Implementing complex form workflows

Tips

  • Always use {% csrf_token %} in forms to prevent CSRF attacks.
  • Use ModelForms for CRUD operations to reduce boilerplate.
  • Add custom validation in clean() and clean_<field>() methods.
  • Use form widgets to customize field rendering (e.g., date pickers, textareas).

Common Pitfalls

  • Not including {% csrf_token %} in forms, causing 403 Forbidden errors.
  • Forgetting to set enctype='multipart/form-data' for file uploads.
  • Not validating file types and sizes, causing security vulnerabilities.
  • Putting validation logic in templates instead of forms.

Summary

  • Django forms provide secure user input handling with validation.
  • ModelForms automatically generate fields from models.
  • Server-side validation ensures data integrity and security.
  • Proper form handling is essential for secure applications.

Exercise

Create comprehensive Django forms for the blog application including post creation/editing, comment submission, and user registration with proper validation.

from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from .models import Post, Comment, Category

class PostForm(forms.ModelForm):
    """Form for creating and editing blog posts."""
    
    class Meta:
        model = Post
        fields = ['title', 'content', 'excerpt', 'category', 'status', 'featured_image', 'tags']
        widgets = {
            'title': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': 'Enter post title'
            }),
            'content': forms.Textarea(attrs={
                'class': 'form-control',
                'rows': 10,
                'placeholder': 'Write your post content here...'
            }),
            'excerpt': forms.Textarea(attrs={
                'class': 'form-control',
                'rows': 3,
                'placeholder': 'Brief summary of your post (optional)'
            }),
            'category': forms.Select(attrs={
                'class': 'form-select'
            }),
            'status': forms.Select(attrs={
                'class': 'form-select'
            }),
            'featured_image': forms.FileInput(attrs={
                'class': 'form-control',
                'accept': 'image/*'
            }),
            'tags': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': 'Enter tags separated by commas'
            })
        }
    
    def clean_title(self):
        """Validate post title."""
        title = self.cleaned_data.get('title')
        if len(title) < 5:
            raise ValidationError('Title must be at least 5 characters long.')
        if len(title) > 200:
            raise ValidationError('Title cannot exceed 200 characters.')
        return title
    
    def clean_content(self):
        """Validate post content."""
        content = self.cleaned_data.get('content')
        if len(content) < 50:
            raise ValidationError('Post content must be at least 50 characters long.')
        return content
    
    def clean_tags(self):
        """Process and validate tags."""
        tags = self.cleaned_data.get('tags')
        if tags:
            # Split by comma and clean up
            tag_list = [tag.strip() for tag in tags.split(',') if tag.strip()]
            # Remove duplicates
            tag_list = list(set(tag_list))
            # Limit number of tags
            if len(tag_list) > 10:
                raise ValidationError('You can only have up to 10 tags.')
            return ', '.join(tag_list)
        return ''

class CommentForm(forms.ModelForm):
    """Form for submitting comments."""
    
    class Meta:
        model = Comment
        fields = ['author_name', 'author_email', 'content']
        widgets = {
            'author_name': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': 'Your name'
            }),
            'author_email': forms.EmailInput(attrs={
                'class': 'form-control',
                'placeholder': 'your.email@example.com'
            }),
            'content': forms.Textarea(attrs={
                'class': 'form-control',
                'rows': 4,
                'placeholder': 'Write your comment here...'
            })
        }
    
    def clean_content(self):
        """Validate comment content."""
        content = self.cleaned_data.get('content')
        if len(content.strip()) < 10:
            raise ValidationError('Comment must be at least 10 characters long.')
        if len(content) > 1000:
            raise ValidationError('Comment cannot exceed 1000 characters.')
        return content
    
    def clean_author_name(self):
        """Validate author name."""
        name = self.cleaned_data.get('author_name')
        if len(name.strip()) < 2:
            raise ValidationError('Name must be at least 2 characters long.')
        return name.strip()

class ContactForm(forms.Form):
    """Contact form for general inquiries."""
    
    SUBJECT_CHOICES = [
        ('general', 'General Inquiry'),
        ('technical', 'Technical Support'),
        ('feedback', 'Feedback'),
        ('other', 'Other')
    ]
    
    name = forms.CharField(
        max_length=100,
        widget=forms.TextInput(attrs={
            'class': 'form-control',
            'placeholder': 'Your full name'
        })
    )
    email = forms.EmailField(
        widget=forms.EmailInput(attrs={
            'class': 'form-control',
            'placeholder': 'your.email@example.com'
        })
    )
    subject = forms.ChoiceField(
        choices=SUBJECT_CHOICES,
        widget=forms.Select(attrs={
            'class': 'form-select'
        })
    )
    message = forms.CharField(
        widget=forms.Textarea(attrs={
            'class': 'form-control',
            'rows': 5,
            'placeholder': 'Your message here...'
        })
    )
    
    def clean_name(self):
        """Validate name field."""
        name = self.cleaned_data.get('name')
        if not name.replace(' ', '').isalpha():
            raise ValidationError('Name can only contain letters and spaces.')
        return name.strip()
    
    def clean_message(self):
        """Validate message field."""
        message = self.cleaned_data.get('message')
        if len(message.strip()) < 20:
            raise ValidationError('Message must be at least 20 characters long.')
        return message

class UserRegistrationForm(UserCreationForm):
    """Custom user registration form."""
    
    email = forms.EmailField(required=True)
    first_name = forms.CharField(max_length=30, required=True)
    last_name = forms.CharField(max_length=30, required=True)
    
    class Meta:
        model = User
        fields = ['username', 'first_name', 'last_name', 'email', 'password1', 'password2']
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Add Bootstrap classes to all fields
        for field in self.fields.values():
            field.widget.attrs.update({'class': 'form-control'})
    
    def clean_email(self):
        """Validate email uniqueness."""
        email = self.cleaned_data.get('email')
        if User.objects.filter(email=email).exists():
            raise ValidationError('This email address is already registered.')
        return email
    
    def save(self, commit=True):
        """Save user with additional fields."""
        user = super().save(commit=False)
        user.email = self.cleaned_data['email']
        user.first_name = self.cleaned_data['first_name']
        user.last_name = self.cleaned_data['last_name']
        if commit:
            user.save()
        return user

class UserProfileForm(forms.ModelForm):
    """Form for updating user profile information."""
    
    class Meta:
        model = User
        fields = ['first_name', 'last_name', 'email']
        widgets = {
            'first_name': forms.TextInput(attrs={'class': 'form-control'}),
            'last_name': forms.TextInput(attrs={'class': 'form-control'}),
            'email': forms.EmailInput(attrs={'class': 'form-control'})
        }
    
    def clean_email(self):
        """Validate email uniqueness excluding current user."""
        email = self.cleaned_data.get('email')
        current_user = self.instance
        if User.objects.exclude(pk=current_user.pk).filter(email=email).exists():
            raise ValidationError('This email address is already registered by another user.')
        return email

# Form for search functionality
class SearchForm(forms.Form):
    """Search form for finding posts."""
    
    q = forms.CharField(
        max_length=100,
        required=False,
        widget=forms.TextInput(attrs={
            'class': 'form-control',
            'placeholder': 'Search posts...',
            'aria-label': 'Search'
        })
    )
    category = forms.ModelChoiceField(
        queryset=Category.objects.all(),
        required=False,
        empty_label="All Categories",
        widget=forms.Select(attrs={
            'class': 'form-select'
        })
    )
    date_from = forms.DateField(
        required=False,
        widget=forms.DateInput(attrs={
            'class': 'form-control',
            'type': 'date'
        })
    )
    date_to = forms.DateField(
        required=False,
        widget=forms.DateInput(attrs={
            'class': 'form-control',
            'type': 'date'
        })
    )
    
    def clean(self):
        """Validate date range."""
        cleaned_data = super().clean()
        date_from = cleaned_data.get('date_from')
        date_to = cleaned_data.get('date_to')
        
        if date_from and date_to and date_from > date_to:
            raise ValidationError('Start date cannot be after end date.')
        
        return cleaned_data

Exercise Tips

  • Use forms.ModelChoiceField for foreign key relationships in forms.
  • Add help_text to form fields for better user experience.
  • Use forms.FileField with proper validators for secure file uploads.
  • Implement form preview functionality for complex multi-step forms.

Code Editor

Output