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 dataTạ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.bioForm 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 ageForm-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_dataComplex 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_dataForm 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 messageBà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 datesBà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 availabilityBà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 databaseTài Liệu Tham Khảo
Previous: Bài 11: CRUD Operations | Next: Bài 13: Model Forms