Class Based Views

Handling Forms with Class-Based Views

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.

Handling Forms with Class-Based Views

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.

FormView - Basic Form Handling

Simple FormView

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

Advanced FormView Patterns

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()}

Multi-Step Form Views

Session-Based Multi-Step Forms

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

Form Validation and Error Handling

Custom Validation in Views

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)

Form Integration with Models

ModelForm Views

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.