Class Based Views

Introduction to Class-Based Views

Class-based views (CBVs) provide an object-oriented approach to handling HTTP requests in Django. They leverage Python's class inheritance to create reusable, composable view components that reduce code duplication and improve maintainability.

Introduction to Class-Based Views

Class-based views (CBVs) provide an object-oriented approach to handling HTTP requests in Django. They leverage Python's class inheritance to create reusable, composable view components that reduce code duplication and improve maintainability.

CBV Fundamentals

Basic CBV Structure

from django.views.generic import View
from django.http import HttpResponse
from django.shortcuts import render

class BasicView(View):
    """Most basic class-based view"""
    
    def get(self, request, *args, **kwargs):
        """Handle GET requests"""
        return HttpResponse("Hello from a class-based view!")
    
    def post(self, request, *args, **kwargs):
        """Handle POST requests"""
        return HttpResponse("POST request received!")

# URL configuration
from django.urls import path

urlpatterns = [
    path('basic/', BasicView.as_view(), name='basic_view'),
]

Method Dispatch

CBVs automatically route HTTP methods to corresponding class methods:

class BlogView(View):
    """Demonstrate HTTP method dispatch"""
    
    def get(self, request, *args, **kwargs):
        """Handle GET requests - display blog posts"""
        posts = Post.objects.filter(status='published')
        return render(request, 'blog/post_list.html', {'posts': posts})
    
    def post(self, request, *args, **kwargs):
        """Handle POST requests - create new post"""
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            post.save()
            return redirect('blog:post_detail', pk=post.pk)
        
        # If form is invalid, redisplay with errors
        posts = Post.objects.filter(status='published')
        return render(request, 'blog/post_list.html', {
            'posts': posts,
            'form': form
        })
    
    def put(self, request, *args, **kwargs):
        """Handle PUT requests - update existing post"""
        # PUT logic here
        return HttpResponse("PUT request handled")
    
    def delete(self, request, *args, **kwargs):
        """Handle DELETE requests - remove post"""
        # DELETE logic here
        return HttpResponse("DELETE request handled")

# Equivalent function-based view
def blog_view_fbv(request):
    """Function-based equivalent"""
    if request.method == 'GET':
        posts = Post.objects.filter(status='published')
        return render(request, 'blog/post_list.html', {'posts': posts})
    
    elif request.method == 'POST':
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            post.save()
            return redirect('blog:post_detail', pk=post.pk)
        
        posts = Post.objects.filter(status='published')
        return render(request, 'blog/post_list.html', {
            'posts': posts,
            'form': form
        })
    
    elif request.method == 'PUT':
        return HttpResponse("PUT request handled")
    
    elif request.method == 'DELETE':
        return HttpResponse("DELETE request handled")

Class Attributes vs Method Overrides

class PostListView(ListView):
    """Demonstrate class attributes vs method overrides"""
    
    # Class attributes (declarative approach)
    model = Post
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    paginate_by = 10
    ordering = ['-created_at']
    
    # Method overrides (programmatic approach)
    def get_queryset(self):
        """Override to add custom filtering"""
        queryset = super().get_queryset()
        
        # Filter by status
        queryset = queryset.filter(status='published')
        
        # Add search functionality
        search_query = self.request.GET.get('search')
        if search_query:
            queryset = queryset.filter(
                Q(title__icontains=search_query) | 
                Q(content__icontains=search_query)
            )
        
        return queryset
    
    def get_context_data(self, **kwargs):
        """Add extra context data"""
        context = super().get_context_data(**kwargs)
        
        # Add search query to context
        context['search_query'] = self.request.GET.get('search', '')
        
        # Add categories for sidebar
        context['categories'] = Category.objects.all()
        
        # Add recent posts
        context['recent_posts'] = Post.objects.filter(
            status='published'
        ).order_by('-created_at')[:5]
        
        return context
    
    def get_paginate_by(self, queryset):
        """Dynamic pagination based on user preference"""
        # Allow users to choose page size
        per_page = self.request.GET.get('per_page', self.paginate_by)
        try:
            per_page = int(per_page)
            # Limit to reasonable range
            return min(max(per_page, 5), 50)
        except (ValueError, TypeError):
            return self.paginate_by

CBV Lifecycle and Method Flow

Request Processing Flow

class DetailedView(View):
    """Demonstrate CBV request processing lifecycle"""
    
    def setup(self, request, *args, **kwargs):
        """Initialize view instance"""
        print("1. setup() - Initialize view")
        super().setup(request, *args, **kwargs)
        
        # Custom initialization
        self.user_agent = request.META.get('HTTP_USER_AGENT', '')
        self.is_mobile = 'mobile' in self.user_agent.lower()
    
    def dispatch(self, request, *args, **kwargs):
        """Route request to appropriate method"""
        print("2. dispatch() - Route to method")
        
        # Custom dispatch logic
        if not request.user.is_authenticated and request.method == 'POST':
            return redirect('accounts:login')
        
        return super().dispatch(request, *args, **kwargs)
    
    def get(self, request, *args, **kwargs):
        """Handle GET request"""
        print("3. get() - Handle GET request")
        
        context = {
            'is_mobile': self.is_mobile,
            'user_agent': self.user_agent,
        }
        
        return render(request, 'example/detail.html', context)
    
    def post(self, request, *args, **kwargs):
        """Handle POST request"""
        print("3. post() - Handle POST request")
        
        # Process form data
        return HttpResponse("POST processed")

# Method execution order:
# 1. setup()
# 2. dispatch()
# 3. get()/post()/put()/delete() etc.

Generic View Lifecycle

class PostDetailView(DetailView):
    """Demonstrate generic view lifecycle"""
    model = Post
    template_name = 'blog/post_detail.html'
    context_object_name = 'post'
    
    def setup(self, request, *args, **kwargs):
        """1. Initialize view"""
        print("1. setup()")
        super().setup(request, *args, **kwargs)
    
    def dispatch(self, request, *args, **kwargs):
        """2. Route request"""
        print("2. dispatch()")
        return super().dispatch(request, *args, **kwargs)
    
    def get_object(self, queryset=None):
        """3. Retrieve object"""
        print("3. get_object()")
        obj = super().get_object(queryset)
        
        # Increment view count
        obj.views += 1
        obj.save(update_fields=['views'])
        
        return obj
    
    def get_context_data(self, **kwargs):
        """4. Build context"""
        print("4. get_context_data()")
        context = super().get_context_data(**kwargs)
        
        # Add related posts
        context['related_posts'] = Post.objects.filter(
            category=self.object.category
        ).exclude(pk=self.object.pk)[:3]
        
        return context
    
    def get_template_names(self):
        """5. Determine template"""
        print("5. get_template_names()")
        
        # Custom template selection logic
        if self.request.user.is_staff:
            return ['blog/post_detail_admin.html']
        
        return super().get_template_names()
    
    def render_to_response(self, context, **response_kwargs):
        """6. Render response"""
        print("6. render_to_response()")
        return super().render_to_response(context, **response_kwargs)

# Generic view method execution order:
# 1. setup()
# 2. dispatch()
# 3. get_object() (for detail views)
# 4. get_context_data()
# 5. get_template_names()
# 6. render_to_response()

CBV Patterns and Best Practices

Attribute Configuration Pattern

class PostListView(ListView):
    """Configure view through class attributes"""
    
    # Model configuration
    model = Post
    queryset = Post.objects.select_related('author', 'category')
    
    # Template configuration
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    
    # Pagination configuration
    paginate_by = 12
    paginate_orphans = 3
    page_kwarg = 'page'
    
    # Ordering configuration
    ordering = ['-created_at', 'title']
    
    # Additional configuration
    allow_empty = True
    extra_context = {
        'page_title': 'Blog Posts',
        'show_sidebar': True,
    }

Method Override Pattern

class CustomPostListView(ListView):
    """Customize view through method overrides"""
    model = Post
    template_name = 'blog/post_list.html'
    
    def get_queryset(self):
        """Custom queryset with filtering"""
        queryset = super().get_queryset()
        
        # Filter by category if provided
        category_slug = self.kwargs.get('category_slug')
        if category_slug:
            queryset = queryset.filter(category__slug=category_slug)
        
        # Filter by author if provided
        author_id = self.request.GET.get('author')
        if author_id:
            queryset = queryset.filter(author_id=author_id)
        
        # Filter by date range
        date_from = self.request.GET.get('date_from')
        date_to = self.request.GET.get('date_to')
        
        if date_from:
            queryset = queryset.filter(created_at__gte=date_from)
        if date_to:
            queryset = queryset.filter(created_at__lte=date_to)
        
        return queryset.filter(status='published')
    
    def get_template_names(self):
        """Dynamic template selection"""
        # Use different template for AJAX requests
        if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
            return ['blog/post_list_ajax.html']
        
        # Use mobile template for mobile devices
        user_agent = self.request.META.get('HTTP_USER_AGENT', '').lower()
        if any(device in user_agent for device in ['mobile', 'android', 'iphone']):
            return ['blog/post_list_mobile.html']
        
        return super().get_template_names()
    
    def get_context_data(self, **kwargs):
        """Add custom context data"""
        context = super().get_context_data(**kwargs)
        
        # Add filter information
        context.update({
            'category_slug': self.kwargs.get('category_slug'),
            'selected_author': self.request.GET.get('author'),
            'date_from': self.request.GET.get('date_from'),
            'date_to': self.request.GET.get('date_to'),
        })
        
        # Add sidebar data
        context.update({
            'categories': Category.objects.annotate(
                post_count=Count('posts', filter=Q(posts__status='published'))
            ),
            'authors': User.objects.filter(
                posts__status='published'
            ).annotate(
                post_count=Count('posts')
            ).distinct(),
            'archive_dates': Post.objects.filter(
                status='published'
            ).dates('created_at', 'month', order='DESC')[:12],
        })
        
        return context
    
    def get_paginate_by(self, queryset):
        """Dynamic pagination"""
        # Allow URL parameter to override pagination
        per_page = self.request.GET.get('per_page')
        if per_page:
            try:
                per_page = int(per_page)
                return min(max(per_page, 5), 100)  # Between 5 and 100
            except ValueError:
                pass
        
        return super().get_paginate_by(queryset)

Mixin Composition Pattern

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.messages.views import SuccessMessageMixin

class AuthorRequiredMixin(UserPassesTestMixin):
    """Mixin to require post author or staff"""
    
    def test_func(self):
        obj = self.get_object()
        return (
            obj.author == self.request.user or 
            self.request.user.is_staff
        )

class PostUpdateView(LoginRequiredMixin, AuthorRequiredMixin, 
                    SuccessMessageMixin, UpdateView):
    """Compose functionality through mixins"""
    model = Post
    form_class = PostForm
    template_name = 'blog/post_form.html'
    success_message = "Post updated successfully!"
    
    def get_success_url(self):
        return reverse('blog:post_detail', kwargs={'pk': self.object.pk})

class AjaxResponseMixin:
    """Mixin to handle AJAX requests"""
    
    def dispatch(self, request, *args, **kwargs):
        if not request.headers.get('X-Requested-With') == 'XMLHttpRequest':
            return HttpResponseBadRequest("AJAX request required")
        return super().dispatch(request, *args, **kwargs)
    
    def form_valid(self, form):
        response = super().form_valid(form)
        
        if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
            return JsonResponse({
                'success': True,
                'redirect_url': self.get_success_url()
            })
        
        return response
    
    def form_invalid(self, form):
        response = super().form_invalid(form)
        
        if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
            return JsonResponse({
                'success': False,
                'errors': form.errors
            })
        
        return response

class AjaxPostCreateView(LoginRequiredMixin, AjaxResponseMixin, CreateView):
    """AJAX-enabled post creation"""
    model = Post
    form_class = PostForm
    
    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)

Common CBV Patterns

API-Style Views

from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
import json

@method_decorator(csrf_exempt, name='dispatch')
class PostAPIView(View):
    """API-style view handling multiple HTTP methods"""
    
    def get(self, request, pk=None):
        """Retrieve post(s)"""
        if pk:
            # Single post
            try:
                post = Post.objects.get(pk=pk, status='published')
                data = {
                    'id': post.id,
                    'title': post.title,
                    'content': post.content,
                    'author': post.author.username,
                    'created_at': post.created_at.isoformat(),
                }
                return JsonResponse(data)
            except Post.DoesNotExist:
                return JsonResponse({'error': 'Post not found'}, status=404)
        else:
            # List of posts
            posts = Post.objects.filter(status='published')[:20]
            data = {
                'posts': [
                    {
                        'id': post.id,
                        'title': post.title,
                        'author': post.author.username,
                        'created_at': post.created_at.isoformat(),
                    }
                    for post in posts
                ]
            }
            return JsonResponse(data)
    
    def post(self, request):
        """Create new post"""
        try:
            data = json.loads(request.body)
            
            post = Post.objects.create(
                title=data['title'],
                content=data['content'],
                author=request.user,
                status='published'
            )
            
            return JsonResponse({
                'id': post.id,
                'title': post.title,
                'created_at': post.created_at.isoformat(),
            }, status=201)
            
        except (KeyError, json.JSONDecodeError) as e:
            return JsonResponse({'error': str(e)}, status=400)
    
    def put(self, request, pk):
        """Update existing post"""
        try:
            post = Post.objects.get(pk=pk)
            data = json.loads(request.body)
            
            post.title = data.get('title', post.title)
            post.content = data.get('content', post.content)
            post.save()
            
            return JsonResponse({
                'id': post.id,
                'title': post.title,
                'updated_at': post.updated_at.isoformat(),
            })
            
        except Post.DoesNotExist:
            return JsonResponse({'error': 'Post not found'}, status=404)
        except json.JSONDecodeError:
            return JsonResponse({'error': 'Invalid JSON'}, status=400)
    
    def delete(self, request, pk):
        """Delete post"""
        try:
            post = Post.objects.get(pk=pk)
            post.delete()
            return JsonResponse({'message': 'Post deleted'}, status=204)
        except Post.DoesNotExist:
            return JsonResponse({'error': 'Post not found'}, status=404)

Multi-Step Form Views

class MultiStepFormView(View):
    """Handle multi-step form workflow"""
    
    def get(self, request):
        """Display current step"""
        step = request.session.get('form_step', 1)
        
        if step == 1:
            form = StepOneForm()
            template = 'forms/step_one.html'
        elif step == 2:
            form = StepTwoForm()
            template = 'forms/step_two.html'
        elif step == 3:
            form = StepThreeForm()
            template = 'forms/step_three.html'
        else:
            # Invalid step, reset
            request.session['form_step'] = 1
            return redirect('multi_step_form')
        
        context = {
            'form': form,
            'step': step,
            'total_steps': 3,
        }
        
        return render(request, template, context)
    
    def post(self, request):
        """Process current step"""
        step = request.session.get('form_step', 1)
        
        if step == 1:
            form = StepOneForm(request.POST)
            if form.is_valid():
                request.session['step_one_data'] = form.cleaned_data
                request.session['form_step'] = 2
                return redirect('multi_step_form')
        
        elif step == 2:
            form = StepTwoForm(request.POST)
            if form.is_valid():
                request.session['step_two_data'] = form.cleaned_data
                request.session['form_step'] = 3
                return redirect('multi_step_form')
        
        elif step == 3:
            form = StepThreeForm(request.POST)
            if form.is_valid():
                # Combine all step data
                step_one_data = request.session.get('step_one_data', {})
                step_two_data = request.session.get('step_two_data', {})
                step_three_data = form.cleaned_data
                
                # Process complete form
                self.process_complete_form(
                    step_one_data, step_two_data, step_three_data
                )
                
                # Clear session data
                for key in ['form_step', 'step_one_data', 'step_two_data']:
                    request.session.pop(key, None)
                
                messages.success(request, 'Form submitted successfully!')
                return redirect('form_success')
        
        # Form invalid, redisplay current step
        template = f'forms/step_{step}.html'
        context = {
            'form': form,
            'step': step,
            'total_steps': 3,
        }
        
        return render(request, template, context)
    
    def process_complete_form(self, step_one, step_two, step_three):
        """Process the complete multi-step form data"""
        # Combine and process all form data
        complete_data = {**step_one, **step_two, **step_three}
        
        # Create database records, send emails, etc.
        # Implementation depends on your specific needs
        pass

Class-based views provide a powerful foundation for building Django applications. Understanding the method dispatch system, lifecycle, and composition patterns enables you to create maintainable, reusable view components that handle complex requirements efficiently.