Learn Django forms, form validation, user authentication, permissions, and security best practices for web development.
Learn Django forms, form validation, user authentication, permissions, and security best practices for web development.
Learn how to create and validate Django forms for handling user input in web applications.
Content by: Manali Trivedi
Python Django Developer
Django forms handle form rendering, validation, and data processing. They provide a secure and efficient way to handle user input in web applications.
# forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from .models import Post, Comment
# Basic form
class ContactForm(forms.Form):
name = forms.CharField(max_length=100, widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Your name'
}))
email = forms.EmailField(widget=forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': 'your@email.com'
}))
subject = forms.CharField(max_length=200, widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Subject'
}))
message = forms.CharField(widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 5,
'placeholder': 'Your message'
}))
def clean_email(self):
email = self.cleaned_data.get('email')
if email and 'spam' in email:
raise forms.ValidationError("Spam emails are not allowed.")
return email
# Model form
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'category', 'published_date']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
'category': forms.Select(attrs={'class': 'form-control'}),
'published_date': forms.DateTimeInput(attrs={
'class': 'form-control',
'type': 'datetime-local'
})
}
def clean_title(self):
title = self.cleaned_data.get('title')
if len(title) < 5:
raise forms.ValidationError("Title must be at least 5 characters long.")
return title
# User registration form
class CustomUserCreationForm(UserCreationForm):
email = forms.EmailField(required=True, widget=forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': 'Email address'
}))
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2']
widgets = {
'username': forms.TextInput(attrs={'class': 'form-control'})
}
# Comment form
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ['content']
widgets = {
'content': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Write your comment...'
})
}
# File upload form
class FileUploadForm(forms.Form):
title = forms.CharField(max_length=100)
description = forms.CharField(widget=forms.Textarea, required=False)
file = forms.FileField(
widget=forms.FileInput(attrs={'class': 'form-control'}),
help_text='Upload your file here'
)
def clean_file(self):
file = self.cleaned_data.get('file')
if file:
if file.size > 5 * 1024 * 1024: # 5MB limit
raise forms.ValidationError("File size must be under 5MB.")
if not file.name.endswith(('.pdf', '.doc', '.docx')):
raise forms.ValidationError("Only PDF and Word documents are allowed.")
return file
Test your understanding of this topic:
Explore how to process and validate forms in Django views to create interactive web applications.
Content by: Manali Trivedi
Python Django Developer
Django views handle form processing, validation, and data saving. Understanding form handling is crucial for building interactive web applications.
# views.py
from django.shortcuts import render, redirect
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from .forms import ContactForm, PostForm, CommentForm
from .models import Post, Comment
# Contact form view
def contact(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
# Process the form data
name = form.cleaned_data['name']
email = form.cleaned_data['email']
subject = form.cleaned_data['subject']
message = form.cleaned_data['message']
# Send email or save to database
# send_contact_email(name, email, subject, message)
messages.success(request, 'Thank you for your message!')
return redirect('contact')
else:
form = ContactForm()
return render(request, 'blog/contact.html', {'form': form})
# Post creation view
@login_required
def post_create(request):
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
messages.success(request, 'Post created successfully!')
return redirect('post_detail', pk=post.pk)
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form, 'title': 'Create Post'})
# Post update view
@login_required
def post_update(request, pk):
post = get_object_or_404(Post, pk=pk)
# Check if user is the author
if post.author != request.user:
messages.error(request, 'You can only edit your own posts.')
return redirect('post_detail', pk=pk)
if request.method == 'POST':
form = PostForm(request.POST, instance=post)
if form.is_valid():
form.save()
messages.success(request, 'Post updated successfully!')
return redirect('post_detail', pk=post.pk)
else:
form = PostForm(instance=post)
return render(request, 'blog/post_form.html', {
'form': form,
'title': 'Edit Post',
'post': post
})
# Comment form view
@login_required
def add_comment(request, post_pk):
post = get_object_or_404(Post, pk=post_pk)
if request.method == 'POST':
form = CommentForm(request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.post = post
comment.author = request.user
comment.save()
messages.success(request, 'Comment added successfully!')
return redirect('post_detail', pk=post_pk)
else:
form = CommentForm()
return render(request, 'blog/post_detail.html', {
'post': post,
'comment_form': form
})
# File upload view
@login_required
def file_upload(request):
if request.method == 'POST':
form = FileUploadForm(request.POST, request.FILES)
if form.is_valid():
# Handle file upload
uploaded_file = form.cleaned_data['file']
# Save file and create database record
messages.success(request, 'File uploaded successfully!')
return redirect('file_list')
else:
form = FileUploadForm()
return render(request, 'blog/file_upload.html', {'form': form})
# AJAX form handling
from django.http import JsonResponse
def ajax_contact(request):
if request.method == 'POST' and request.is_ajax():
form = ContactForm(request.POST)
if form.is_valid():
# Process form data
return JsonResponse({'success': True, 'message': 'Message sent successfully!'})
else:
return JsonResponse({'success': False, 'errors': form.errors})
return JsonResponse({'success': False, 'message': 'Invalid request'})
Test your understanding of this topic:
Master Django's authentication system for user management, permissions, and session handling.
Content by: Manali Trivedi
Python Django Developer
Django provides a comprehensive authentication system that handles user accounts, groups, permissions, and sessions. It's secure, flexible, and easy to extend.
# views.py
from django.contrib.auth import login, logout, authenticate
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.shortcuts import render, redirect
from .forms import CustomUserCreationForm
# User registration
def register(request):
if request.method == 'POST':
form = CustomUserCreationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
messages.success(request, 'Account created successfully!')
return redirect('home')
else:
form = CustomUserCreationForm()
return render(request, 'registration/register.html', {'form': form})
# User login
def user_login(request):
if request.method == 'POST':
form = AuthenticationForm(request, data=request.POST)
if form.is_valid():
username = form.cleaned_data.get('username')
password = form.cleaned_data.get('password')
user = authenticate(username=username, password=password)
if user is not None:
login(request, user)
messages.success(request, f'Welcome back, {username}!')
return redirect('home')
else:
form = AuthenticationForm()
return render(request, 'registration/login.html', {'form': form})
# User logout
@login_required
def user_logout(request):
logout(request)
messages.info(request, 'You have been logged out.')
return redirect('home')
# Profile view
@login_required
def profile(request):
return render(request, 'registration/profile.html', {'user': request.user})
# Password change
from django.contrib.auth.forms import PasswordChangeForm
@login_required
def password_change(request):
if request.method == 'POST':
form = PasswordChangeForm(request.user, request.POST)
if form.is_valid():
user = form.save()
update_session_auth_hash(request, user)
messages.success(request, 'Password changed successfully!')
return redirect('profile')
else:
form = PasswordChangeForm(request.user)
return render(request, 'registration/password_change.html', {'form': form})
# Custom user model (optional)
from django.contrib.auth.models import AbstractUser
from django.db import models
class CustomUser(AbstractUser):
bio = models.TextField(max_length=500, blank=True)
birth_date = models.DateField(null=True, blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True)
def __str__(self):
return self.username
# User permissions and groups
from django.contrib.auth.models import Group, Permission
# Create a group
editors_group, created = Group.objects.get_or_create(name='Editors')
# Add permissions to group
post_permissions = Permission.objects.filter(
content_type__app_label='blog',
content_type__model='post'
)
editors_group.permissions.set(post_permissions)
# Add user to group
user.groups.add(editors_group)
# Check permissions in views
@login_required
def admin_panel(request):
if request.user.has_perm('blog.can_publish_post'):
# Show admin panel
return render(request, 'admin/panel.html')
else:
messages.error(request, 'You do not have permission to access this page.')
return redirect('home')
# Custom permission check
def can_edit_post(user, post):
return user == post.author or user.has_perm('blog.can_edit_all_posts')
@login_required
def post_edit(request, pk):
post = get_object_or_404(Post, pk=pk)
if not can_edit_post(request.user, post):
messages.error(request, 'You do not have permission to edit this post.')
return redirect('post_detail', pk=pk)
# Continue with edit logic...
Test your understanding of this topic:
Learn Django's built-in security features and best practices to protect your web applications from vulnerabilities.
Content by: Manali Trivedi
Python Django Developer
Django includes many built-in security features to protect your applications from common web vulnerabilities. Understanding these features is essential for building secure applications.
# settings.py - Security Settings
import os
from pathlib import Path
# Security settings
SECRET_KEY = os.environ.get('SECRET_KEY', 'your-secret-key-here')
# HTTPS settings
SECURE_SSL_REDIRECT = True # Redirect HTTP to HTTPS
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Cookie settings
SESSION_COOKIE_SECURE = True # Only send cookies over HTTPS
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True # Prevent XSS
CSRF_COOKIE_HTTPONLY = True
# Content Security Policy
CSP_DEFAULT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
CSP_SCRIPT_SRC = ("'self'",)
# CSRF Protection
CSRF_TRUSTED_ORIGINS = [
'https://yourdomain.com',
'https://www.yourdomain.com',
]
# XSS Protection
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
# Clickjacking Protection
X_FRAME_OPTIONS = 'DENY'
# SQL Injection Protection (Django ORM handles this automatically)
# Always use Django ORM instead of raw SQL when possible
# Example of secure form handling
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.http import require_http_methods
@csrf_protect
@require_http_methods(["GET", "POST"])
def secure_form(request):
if request.method == 'POST':
form = MyForm(request.POST)
if form.is_valid():
# Process form data
pass
else:
form = MyForm()
return render(request, 'form.html', {'form': form})
# Rate limiting
from django.core.cache import cache
from django.http import HttpResponseTooManyRequests
def rate_limit(view_func):
def wrapper(request, *args, **kwargs):
# Get client IP
client_ip = request.META.get('REMOTE_ADDR')
# Check rate limit
cache_key = f'rate_limit_{client_ip}'
request_count = cache.get(cache_key, 0)
if request_count > 100: # 100 requests per hour
return HttpResponseTooManyRequests('Rate limit exceeded')
# Increment counter
cache.set(cache_key, request_count + 1, 3600) # 1 hour
return view_func(request, *args, **kwargs)
return wrapper
# Input validation
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
def validate_user_input(data):
errors = {}
# Email validation
try:
validate_email(data.get('email', ''))
except ValidationError:
errors['email'] = 'Enter a valid email address.'
# Password strength
password = data.get('password', '')
if len(password) < 8:
errors['password'] = 'Password must be at least 8 characters long.'
elif not any(c.isupper() for c in password):
errors['password'] = 'Password must contain at least one uppercase letter.'
elif not any(c.isdigit() for c in password):
errors['password'] = 'Password must contain at least one number.'
return errors
# File upload security
import os
from django.core.files.storage import default_storage
def secure_file_upload(file):
# Check file size
if file.size > 5 * 1024 * 1024: # 5MB
raise ValidationError('File size must be under 5MB.')
# Check file extension
allowed_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.pdf']
file_extension = os.path.splitext(file.name)[1].lower()
if file_extension not in allowed_extensions:
raise ValidationError('File type not allowed.')
# Generate secure filename
import uuid
secure_filename = f"{uuid.uuid4()}{file_extension}"
# Save file securely
file_path = default_storage.save(f'uploads/{secure_filename}', file)
return file_path
# SQL Injection Prevention
# ❌ Bad - Don't do this
def bad_query(request):
user_input = request.GET.get('search', '')
query = f"SELECT * FROM posts WHERE title LIKE '%{user_input}%'"
# This is vulnerable to SQL injection
# ✅ Good - Use Django ORM
def good_query(request):
user_input = request.GET.get('search', '')
posts = Post.objects.filter(title__icontains=user_input)
# Django ORM automatically escapes user input
# XSS Prevention
# Django templates automatically escape HTML
# {{ user_input }} # Safe - automatically escaped
# {{ user_input|safe }} # Dangerous - only use with trusted content
# CSRF Protection
# Django includes CSRF protection by default
# Include {% csrf_token %} in all forms
# Password hashing
from django.contrib.auth.hashers import make_password, check_password
# Hash password
hashed_password = make_password('my_password')
# Check password
is_valid = check_password('my_password', hashed_password)
Test your understanding of this topic:
Learn advanced form patterns including dynamic forms, form sets, and custom form widgets for complex applications.
Content by: Manali Trivedi
Python Django Developer
Django provides powerful tools for creating dynamic forms, form sets, and custom form widgets. These patterns are essential for building complex, interactive web applications.
# Form sets for multiple objects
from django.forms import formset_factory, BaseFormSet
from django.core.exceptions import ValidationError
class BasePostFormSet(BaseFormSet):
def clean(self):
"""Custom validation for the entire form set"""
super().clean()
# Check if at least one form has data
if not any(form.cleaned_data for form in self.forms):
raise ValidationError("At least one post must be provided.")
# Check for duplicate titles
titles = []
for form in self.forms:
if form.cleaned_data:
title = form.cleaned_data.get('title')
if title in titles:
raise ValidationError("Duplicate titles are not allowed.")
titles.append(title)
# Create form set
PostFormSet = formset_factory(
PostForm,
formset=BasePostFormSet,
extra=3, # Show 3 empty forms
max_num=10, # Maximum 10 forms
can_delete=True # Allow deletion
)
# Dynamic form creation
class DynamicFormBuilder:
"""Build forms dynamically based on configuration"""
def __init__(self):
self.field_types = {
'text': forms.CharField,
'textarea': forms.CharField,
'email': forms.EmailField,
'number': forms.IntegerField,
'choice': forms.ChoiceField,
}
def build_form(self, field_configs):
"""Build a form class from field configurations"""
form_fields = {}
for field_config in field_configs:
field_name = field_config['name']
field_type = field_config['type']
field_options = field_config.get('options', {})
if field_type in self.field_types:
field_class = self.field_types[field_type]
# Handle special cases
if field_type == 'textarea':
field_options['widget'] = forms.Textarea
elif field_type == 'choice':
field_options['choices'] = field_config.get('choices', [])
form_fields[field_name] = field_class(**field_options)
return type('DynamicForm', (forms.Form,), form_fields)
# Custom form widgets
class RichTextWidget(forms.Textarea):
"""Custom widget for rich text editing"""
class Media:
css = {
'all': ('css/rich-text-editor.css',)
}
js = ('js/rich-text-editor.js',)
def __init__(self, attrs=None):
default_attrs = {
'class': 'rich-text-editor',
'data-editor': 'true'
}
if attrs:
default_attrs.update(attrs)
super().__init__(default_attrs)
class ColorPickerWidget(forms.TextInput):
"""Custom widget for color picking"""
class Media:
css = {
'all': ('css/color-picker.css',)
}
js = ('js/color-picker.js',)
def __init__(self, attrs=None):
default_attrs = {
'type': 'color',
'class': 'color-picker'
}
if attrs:
default_attrs.update(attrs)
super().__init__(default_attrs)
# Advanced form validation
class AdvancedPostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'category', 'tags']
def clean(self):
cleaned_data = super().clean()
title = cleaned_data.get('title')
content = cleaned_data.get('content')
# Cross-field validation
if title and content:
# Check for duplicate content
if Post.objects.filter(title__iexact=title).exists():
raise ValidationError("A post with this title already exists.")
# Check content quality
word_count = len(content.split())
if word_count < 50:
raise ValidationError("Content must be at least 50 words long.")
# Check for spam keywords
spam_keywords = ['buy now', 'click here', 'free money']
content_lower = content.lower()
for keyword in spam_keywords:
if keyword in content_lower:
raise ValidationError("Content contains prohibited keywords.")
return cleaned_data
def clean_title(self):
title = self.cleaned_data.get('title')
if title:
# Check title length
if len(title) < 10:
raise ValidationError("Title must be at least 10 characters long.")
# Check for inappropriate words
inappropriate_words = ['spam', 'scam', 'fake']
if any(word in title.lower() for word in inappropriate_words):
raise ValidationError("Title contains inappropriate words.")
return title
# Form processing with AJAX
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
import json
@csrf_exempt
def ajax_form_processing(request):
"""Process forms via AJAX"""
if request.method == 'POST':
try:
data = json.loads(request.body)
form = AdvancedPostForm(data)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
form.save_m2m()
return JsonResponse({
'success': True,
'message': 'Post created successfully!',
'post_id': post.id
})
else:
return JsonResponse({
'success': False,
'errors': form.errors
})
except json.JSONDecodeError:
return JsonResponse({
'success': False,
'message': 'Invalid JSON data'
})
return JsonResponse({
'success': False,
'message': 'Invalid request method'
})
# Usage in views
def create_multiple_posts(request):
"""Create multiple posts using form sets"""
if request.method == 'POST':
formset = PostFormSet(request.POST)
if formset.is_valid():
for form in formset:
if form.cleaned_data:
post = form.save(commit=False)
post.author = request.user
post.save()
messages.success(request, 'Posts created successfully!')
return redirect('post_list')
else:
formset = PostFormSet()
return render(request, 'blog/create_multiple_posts.html', {
'formset': formset
})
# Multi-Step Form Wizard
from django import forms
from django.shortcuts import render, redirect
from django.contrib import messages
from django.core.cache import cache
import uuid
class Step1Form(forms.Form):
"""First step: Basic information"""
title = forms.CharField(max_length=200)
category = forms.ChoiceField(choices=[
('tech', 'Technology'),
('lifestyle', 'Lifestyle'),
('travel', 'Travel')
])
class Step2Form(forms.Form):
"""Second step: Content"""
content = forms.CharField(widget=forms.Textarea)
excerpt = forms.CharField(widget=forms.Textarea, required=False)
class Step3Form(forms.Form):
"""Third step: Publishing options"""
status = forms.ChoiceField(choices=[
('draft', 'Draft'),
('published', 'Published')
])
featured = forms.BooleanField(required=False)
class FormWizard:
"""Multi-step form wizard for creating posts"""
def __init__(self, request):
self.request = request
self.session_key = f"wizard_{request.user.id if request.user.is_authenticated else 'anonymous'}"
self.steps = [Step1Form, Step2Form, Step3Form]
self.current_step = self.get_current_step()
def get_current_step(self):
"""Get the current step from session"""
return self.request.session.get(f"{self.session_key}_step", 0)
def set_current_step(self, step):
"""Set the current step in session"""
self.request.session[f"{self.session_key}_step"] = step
def get_form_data(self):
"""Get all form data from session"""
return self.request.session.get(f"{self.session_key}_data", {})
def set_form_data(self, data):
"""Set form data in session"""
self.request.session[f"{self.session_key}_data"] = data
def get_form(self, step=None):
"""Get the form for the current or specified step"""
if step is None:
step = self.current_step
form_class = self.steps[step]
initial_data = self.get_form_data().get(f"step_{step}", {})
return form_class(initial=initial_data)
def process_step(self, form):
"""Process the current step and save data"""
if form.is_valid():
step_data = self.get_form_data()
step_data[f"step_{self.current_step}"] = form.cleaned_data
self.set_form_data(step_data)
return True
return False
def next_step(self):
"""Move to the next step"""
if self.current_step < len(self.steps) - 1:
self.set_current_step(self.current_step + 1)
return True
return False
def previous_step(self):
"""Move to the previous step"""
if self.current_step > 0:
self.set_current_step(self.current_step - 1)
return True
return False
def is_complete(self):
"""Check if all steps are complete"""
form_data = self.get_form_data()
return len(form_data) == len(self.steps)
def get_final_data(self):
"""Get all form data combined"""
if not self.is_complete():
return None
final_data = {}
form_data = self.get_form_data()
for step in range(len(self.steps)):
step_data = form_data.get(f"step_{step}", {})
final_data.update(step_data)
return final_data
def clear_session(self):
"""Clear wizard session data"""
keys_to_delete = [
f"{self.session_key}_step",
f"{self.session_key}_data"
]
for key in keys_to_delete:
if key in self.request.session:
del self.request.session[key]
# Wizard view
def post_wizard(request):
"""Multi-step form wizard for creating posts"""
wizard = FormWizard(request)
if request.method == 'POST':
if 'next' in request.POST:
# Process current step and move to next
form = wizard.get_form()
if wizard.process_step(form):
if wizard.next_step():
return redirect('post_wizard')
else:
# All steps complete, create post
final_data = wizard.get_final_data()
if final_data:
post = Post.objects.create(
author=request.user,
**final_data
)
wizard.clear_session()
messages.success(request, 'Post created successfully!')
return redirect('post_detail', pk=post.pk)
elif 'previous' in request.POST:
# Move to previous step
wizard.previous_step()
return redirect('post_wizard')
elif 'save_draft' in request.POST:
# Save current progress
form = wizard.get_form()
if wizard.process_step(form):
messages.info(request, 'Progress saved. You can continue later.')
return redirect('post_wizard')
# Get current form
form = wizard.get_form()
# Get progress
progress = (wizard.current_step + 1) / len(wizard.steps) * 100
context = {
'form': form,
'current_step': wizard.current_step + 1,
'total_steps': len(wizard.steps),
'progress': progress,
'can_go_back': wizard.current_step > 0,
'can_go_next': wizard.current_step < len(wizard.steps) - 1,
'is_last_step': wizard.current_step == len(wizard.steps) - 1,
}
return render(request, 'blog/post_wizard.html', context)
# Template for the wizard
"""
{% extends 'base.html' %}
{% block content %}
<div class="wizard-container">
<div class="wizard-progress">
<div class="progress-bar" style="width: {{ progress }}%"></div>
<span class="progress-text">Step {{ current_step }} of {{ total_steps }}</span>
</div>
<form method="post">
{% csrf_token %}
<div class="form-step">
{{ form.as_p }}
</div>
<div class="wizard-actions">
{% if can_go_back %}
<button type="submit" name="previous" class="btn btn-secondary">Previous</button>
{% endif %}
{% if not is_last_step %}
<button type="submit" name="next" class="btn btn-primary">Next</button>
{% else %}
<button type="submit" name="next" class="btn btn-success">Create Post</button>
{% endif %}
<button type="submit" name="save_draft" class="btn btn-info">Save Draft</button>
</div>
</form>
</div>
{% endblock %}
"""
Test your understanding of this topic:
Continue your learning journey and master the next set of concepts.
Continue to Module 6