Django's class-based views provide powerful abstractions for form handling, from simple contact forms to complex multi-step workflows. Understanding form views and their integration patterns is essential for building interactive web applications.
from django.views.generic import FormView
from django.contrib import messages
from django.urls import reverse_lazy
from django.core.mail import send_mail
class ContactFormView(FormView):
"""Basic contact form handling"""
template_name = 'contact/form.html'
form_class = ContactForm
success_url = reverse_lazy('contact:success')
def form_valid(self, form):
"""Process valid form submission"""
# Send email
self.send_contact_email(form.cleaned_data)
# Add success message
messages.success(
self.request,
'Thank you for your message! We\'ll get back to you soon.'
)
return super().form_valid(form)
def send_contact_email(self, form_data):
"""Send contact form email"""
subject = f"Contact Form: {form_data['subject']}"
message = f"""
Name: {form_data['name']}
Email: {form_data['email']}
Subject: {form_data['subject']}
Message:
{form_data['message']}
"""
send_mail(
subject,
message,
'noreply@example.com',
['contact@example.com'],
fail_silently=False,
)
class NewsletterSignupView(FormView):
"""Newsletter signup form"""
template_name = 'newsletter/signup.html'
form_class = NewsletterForm
success_url = reverse_lazy('newsletter:success')
def form_valid(self, form):
"""Process newsletter signup"""
email = form.cleaned_data['email']
# Check if already subscribed
if NewsletterSubscriber.objects.filter(email=email).exists():
messages.info(self.request, 'You are already subscribed to our newsletter.')
else:
# Create subscription
subscriber = NewsletterSubscriber.objects.create(
email=email,
name=form.cleaned_data.get('name', ''),
subscribed_at=timezone.now()
)
# Send confirmation email
self.send_confirmation_email(subscriber)
messages.success(
self.request,
'Thank you for subscribing! Please check your email to confirm.'
)
return super().form_valid(form)
def send_confirmation_email(self, subscriber):
"""Send confirmation email"""
confirmation_url = self.request.build_absolute_uri(
reverse('newsletter:confirm', kwargs={'token': subscriber.confirmation_token})
)
send_mail(
'Confirm your newsletter subscription',
f'Please click this link to confirm: {confirmation_url}',
'noreply@example.com',
[subscriber.email],
)
class FeedbackFormView(FormView):
"""Feedback form with file uploads"""
template_name = 'feedback/form.html'
form_class = FeedbackForm
success_url = reverse_lazy('feedback:success')
def form_valid(self, form):
"""Process feedback with file attachments"""
# Create feedback record
feedback = Feedback.objects.create(
name=form.cleaned_data['name'],
email=form.cleaned_data['email'],
category=form.cleaned_data['category'],
message=form.cleaned_data['message'],
submitted_at=timezone.now()
)
# Handle file attachments
uploaded_files = self.request.FILES.getlist('attachments')
for uploaded_file in uploaded_files:
if self.validate_attachment(uploaded_file):
FeedbackAttachment.objects.create(
feedback=feedback,
file=uploaded_file,
filename=uploaded_file.name
)
# Send notification
self.send_feedback_notification(feedback)
messages.success(self.request, 'Thank you for your feedback!')
return super().form_valid(form)
def validate_attachment(self, uploaded_file):
"""Validate uploaded attachment"""
# Check file size (5MB limit)
if uploaded_file.size > 5 * 1024 * 1024:
messages.error(self.request, f'File {uploaded_file.name} is too large (max 5MB)')
return False
# Check file type
allowed_types = ['image/jpeg', 'image/png', 'application/pdf', 'text/plain']
if uploaded_file.content_type not in allowed_types:
messages.error(self.request, f'File type {uploaded_file.content_type} not allowed')
return False
return True
class AdvancedFormView(FormView):
"""Advanced form handling with multiple features"""
template_name = 'forms/advanced.html'
form_class = AdvancedForm
def get_form_kwargs(self):
"""Pass additional data to form"""
kwargs = super().get_form_kwargs()
# Pass user to form for user-specific validation
kwargs['user'] = self.request.user
# Pass request for IP-based validation
kwargs['request'] = self.request
return kwargs
def get_context_data(self, **kwargs):
"""Add extra context for template"""
context = super().get_context_data(**kwargs)
# Add related data
context.update({
'categories': Category.objects.filter(active=True),
'user_submissions': self.get_user_submissions(),
'form_help_text': self.get_form_help_text(),
})
return context
def get_user_submissions(self):
"""Get user's previous submissions"""
if self.request.user.is_authenticated:
return Submission.objects.filter(
user=self.request.user
).order_by('-created_at')[:5]
return []
def get_form_help_text(self):
"""Get contextual help text"""
return {
'general': 'Please fill out all required fields.',
'file_upload': 'Supported formats: PDF, JPG, PNG (max 5MB)',
'privacy': 'Your information will be kept confidential.',
}
def form_valid(self, form):
"""Enhanced form processing"""
# Create submission record
submission = self.create_submission(form)
# Process based on submission type
self.process_submission_type(submission, form)
# Handle file uploads
self.process_file_uploads(submission)
# Send notifications
self.send_notifications(submission)
# Set success message
self.set_success_message(submission)
return super().form_valid(form)
def create_submission(self, form):
"""Create submission record"""
return Submission.objects.create(
user=self.request.user if self.request.user.is_authenticated else None,
email=form.cleaned_data['email'],
category=form.cleaned_data['category'],
priority=form.cleaned_data.get('priority', 'normal'),
data=form.cleaned_data,
ip_address=self.get_client_ip(),
user_agent=self.request.META.get('HTTP_USER_AGENT', ''),
submitted_at=timezone.now()
)
def process_submission_type(self, submission, form):
"""Process based on submission type"""
submission_type = form.cleaned_data.get('type')
if submission_type == 'urgent':
# Send immediate notification
self.send_urgent_notification(submission)
submission.priority = 'high'
elif submission_type == 'feedback':
# Route to feedback team
submission.assigned_team = 'feedback'
elif submission_type == 'support':
# Create support ticket
self.create_support_ticket(submission)
submission.save()
def get_success_url(self):
"""Dynamic success URL"""
# Redirect based on submission type
submission_type = self.request.POST.get('type')
if submission_type == 'support':
return reverse('support:ticket_created')
elif submission_type == 'feedback':
return reverse('feedback:thank_you')
return reverse('forms:success')
class AjaxFormView(FormView):
"""AJAX-enabled form view"""
template_name = 'forms/ajax_form.html'
form_class = AjaxForm
def form_valid(self, form):
"""Handle AJAX form submission"""
# Process form normally
result = self.process_form(form)
# Return JSON response for AJAX requests
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'success': True,
'message': 'Form submitted successfully!',
'redirect_url': self.get_success_url(),
'result_data': result
})
# Regular form submission
return super().form_valid(form)
def form_invalid(self, form):
"""Handle AJAX form errors"""
response = super().form_invalid(form)
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'success': False,
'errors': form.errors,
'non_field_errors': form.non_field_errors()
})
return response
def process_form(self, form):
"""Process the form and return result data"""
# Your form processing logic here
return {'processed_at': timezone.now().isoformat()}
class MultiStepFormView(FormView):
"""Base class for multi-step forms"""
step_forms = {} # Override in subclass
template_name = 'forms/multi_step.html'
def get_step(self):
"""Get current step from session"""
return self.request.session.get('form_step', 1)
def set_step(self, step):
"""Set current step in session"""
self.request.session['form_step'] = step
def get_form_class(self):
"""Get form class for current step"""
step = self.get_step()
return self.step_forms.get(step, self.step_forms[1])
def get_template_names(self):
"""Get template for current step"""
step = self.get_step()
return [f'forms/step_{step}.html', self.template_name]
def get_context_data(self, **kwargs):
"""Add step information to context"""
context = super().get_context_data(**kwargs)
context.update({
'current_step': self.get_step(),
'total_steps': len(self.step_forms),
'step_progress': (self.get_step() / len(self.step_forms)) * 100,
})
return context
def form_valid(self, form):
"""Handle step completion"""
step = self.get_step()
# Save step data to session
self.save_step_data(step, form.cleaned_data)
# Check if this is the last step
if step >= len(self.step_forms):
return self.process_final_step()
# Move to next step
self.set_step(step + 1)
return redirect(self.request.path)
def save_step_data(self, step, data):
"""Save step data to session"""
session_key = f'step_{step}_data'
self.request.session[session_key] = data
def get_step_data(self, step):
"""Get step data from session"""
session_key = f'step_{step}_data'
return self.request.session.get(session_key, {})
def process_final_step(self):
"""Process all steps when form is complete"""
# Collect all step data
all_data = {}
for step in range(1, len(self.step_forms) + 1):
all_data.update(self.get_step_data(step))
# Process complete form
result = self.process_complete_form(all_data)
# Clear session data
self.clear_session_data()
# Redirect to success page
return redirect(self.get_success_url())
def clear_session_data(self):
"""Clear form data from session"""
keys_to_remove = ['form_step']
for step in range(1, len(self.step_forms) + 1):
keys_to_remove.append(f'step_{step}_data')
for key in keys_to_remove:
self.request.session.pop(key, None)
def process_complete_form(self, data):
"""Override this method to process complete form"""
raise NotImplementedError("Subclasses must implement process_complete_form")
class UserRegistrationWizard(MultiStepFormView):
"""Multi-step user registration"""
step_forms = {
1: PersonalInfoForm,
2: AccountDetailsForm,
3: PreferencesForm,
}
success_url = reverse_lazy('accounts:registration_complete')
def process_complete_form(self, data):
"""Create user account with all collected data"""
# Create user
user = User.objects.create_user(
username=data['username'],
email=data['email'],
password=data['password'],
first_name=data['first_name'],
last_name=data['last_name']
)
# Create profile
UserProfile.objects.create(
user=user,
phone=data.get('phone', ''),
date_of_birth=data.get('date_of_birth'),
newsletter_subscription=data.get('newsletter', False),
email_notifications=data.get('email_notifications', True)
)
# Send welcome email
self.send_welcome_email(user)
# Log user in
login(self.request, user)
return user
def send_welcome_email(self, user):
"""Send welcome email to new user"""
send_mail(
'Welcome to our site!',
f'Hello {user.first_name}, welcome to our community!',
'noreply@example.com',
[user.email],
)
class SurveyWizard(MultiStepFormView):
"""Multi-step survey form"""
step_forms = {
1: DemographicsForm,
2: PreferencesForm,
3: FeedbackForm,
}
success_url = reverse_lazy('survey:thank_you')
def get_form_kwargs(self):
"""Pass previous step data to current form"""
kwargs = super().get_form_kwargs()
# Pass all previous step data for conditional logic
previous_data = {}
current_step = self.get_step()
for step in range(1, current_step):
previous_data.update(self.get_step_data(step))
kwargs['previous_data'] = previous_data
return kwargs
def process_complete_form(self, data):
"""Save survey response"""
survey_response = SurveyResponse.objects.create(
respondent_email=data.get('email'),
age_group=data.get('age_group'),
location=data.get('location'),
preferences=data.get('preferences', {}),
feedback=data.get('feedback', ''),
completed_at=timezone.now()
)
# Process survey logic
self.analyze_survey_response(survey_response)
return survey_response
class ValidatedFormView(FormView):
"""Form view with custom validation"""
template_name = 'forms/validated.html'
form_class = CustomForm
def form_valid(self, form):
"""Additional validation beyond form validation"""
# Business logic validation
if not self.validate_business_rules(form):
return self.form_invalid(form)
# External API validation
if not self.validate_external_data(form):
form.add_error(None, 'External validation failed. Please try again.')
return self.form_invalid(form)
# Rate limiting check
if self.is_rate_limited():
form.add_error(None, 'Too many submissions. Please wait before trying again.')
return self.form_invalid(form)
return super().form_valid(form)
def validate_business_rules(self, form):
"""Custom business rule validation"""
data = form.cleaned_data
# Example: Check if user can submit based on their status
if self.request.user.is_authenticated:
user_profile = getattr(self.request.user, 'profile', None)
if user_profile and not user_profile.can_submit_forms:
form.add_error(None, 'Your account is not authorized to submit forms.')
return False
# Example: Validate data relationships
if data.get('start_date') and data.get('end_date'):
if data['start_date'] >= data['end_date']:
form.add_error('end_date', 'End date must be after start date.')
return False
return True
def validate_external_data(self, form):
"""Validate against external API"""
try:
# Example: Validate address with external service
address_data = {
'street': form.cleaned_data.get('street'),
'city': form.cleaned_data.get('city'),
'postal_code': form.cleaned_data.get('postal_code'),
}
# Mock external API call
is_valid = self.validate_address_external(address_data)
if not is_valid:
form.add_error('street', 'Address could not be validated.')
return False
return True
except Exception as e:
# Log error but don't fail validation
logger.error(f'External validation error: {e}')
return True # Allow form to proceed if external service fails
def is_rate_limited(self):
"""Check if user is rate limited"""
if not self.request.user.is_authenticated:
# Check by IP for anonymous users
ip_address = self.get_client_ip()
recent_submissions = FormSubmission.objects.filter(
ip_address=ip_address,
created_at__gte=timezone.now() - timedelta(hours=1)
).count()
return recent_submissions >= 5 # 5 submissions per hour
else:
# Check by user for authenticated users
recent_submissions = FormSubmission.objects.filter(
user=self.request.user,
created_at__gte=timezone.now() - timedelta(hours=1)
).count()
return recent_submissions >= 10 # 10 submissions per hour
class ConditionalFormView(FormView):
"""Form with conditional field validation"""
template_name = 'forms/conditional.html'
form_class = ConditionalForm
def get_form_kwargs(self):
"""Pass request data for conditional logic"""
kwargs = super().get_form_kwargs()
kwargs['request'] = self.request
return kwargs
def form_valid(self, form):
"""Conditional processing based on form data"""
form_type = form.cleaned_data.get('form_type')
if form_type == 'business':
return self.process_business_form(form)
elif form_type == 'individual':
return self.process_individual_form(form)
else:
return self.process_general_form(form)
def process_business_form(self, form):
"""Process business-specific form"""
# Validate business-specific fields
if not form.cleaned_data.get('company_name'):
form.add_error('company_name', 'Company name is required for business forms.')
return self.form_invalid(form)
# Create business record
business = Business.objects.create(
name=form.cleaned_data['company_name'],
contact_email=form.cleaned_data['email'],
phone=form.cleaned_data.get('phone', ''),
industry=form.cleaned_data.get('industry', ''),
)
# Send business-specific email
self.send_business_confirmation(business)
return super().form_valid(form)
def process_individual_form(self, form):
"""Process individual-specific form"""
# Create individual record
individual = Individual.objects.create(
first_name=form.cleaned_data['first_name'],
last_name=form.cleaned_data['last_name'],
email=form.cleaned_data['email'],
phone=form.cleaned_data.get('phone', ''),
)
# Send individual-specific email
self.send_individual_confirmation(individual)
return super().form_valid(form)
class ModelFormView(FormView):
"""Form view integrated with model operations"""
template_name = 'forms/model_form.html'
form_class = PostModelForm
def get_form_kwargs(self):
"""Pass model instance to form if editing"""
kwargs = super().get_form_kwargs()
# Check if we're editing an existing object
pk = self.kwargs.get('pk')
if pk:
try:
kwargs['instance'] = Post.objects.get(pk=pk)
except Post.DoesNotExist:
raise Http404("Post not found")
return kwargs
def form_valid(self, form):
"""Save model instance"""
# Save the model
instance = form.save(commit=False)
# Add additional data
if not instance.pk: # New instance
instance.author = self.request.user
instance.created_at = timezone.now()
instance.updated_at = timezone.now()
instance.save()
# Save many-to-many relationships
form.save_m2m()
# Store instance for success URL
self.object = instance
return super().form_valid(form)
def get_success_url(self):
"""Redirect to object detail page"""
return self.object.get_absolute_url()
class InlineFormsetView(FormView):
"""Handle formsets for related objects"""
template_name = 'forms/formset.html'
form_class = AuthorForm
def get_context_data(self, **kwargs):
"""Add formset to context"""
context = super().get_context_data(**kwargs)
if self.request.POST:
context['book_formset'] = BookFormSet(self.request.POST)
else:
context['book_formset'] = BookFormSet()
return context
def form_valid(self, form):
"""Validate both form and formset"""
context = self.get_context_data()
book_formset = context['book_formset']
if book_formset.is_valid():
# Save author
author = form.save()
# Save books
books = book_formset.save(commit=False)
for book in books:
book.author = author
book.save()
# Handle deleted books
for book in book_formset.deleted_objects:
book.delete()
return super().form_valid(form)
else:
return self.form_invalid(form)
Form handling in class-based views provides powerful abstractions for user input processing. From simple contact forms to complex multi-step workflows, understanding FormView patterns enables you to build robust, user-friendly interfaces with proper validation, error handling, and integration with your data models.
Views for CRUD Operations
Django's generic views provide comprehensive support for Create, Read, Update, and Delete (CRUD) operations. These views handle the common patterns of object lifecycle management with minimal code while maintaining flexibility for customization.
Using Mixins
Mixins are a powerful feature of Django's class-based views that enable code reuse through multiple inheritance. They provide a way to compose functionality from different sources, creating flexible and maintainable view hierarchies.