Bài 19: Generic Views

Generic Views Là Gì?

Generic Views (Class-Based Views) là pre-built views cho common patterns, giúp viết code nhanh hơn.

# Function-based view (FBV)def post_list(request):    posts = Post.objects.all()    return render(request, 'post_list.html', {'posts': posts}) # Class-based view (CBV) - Generic ListViewfrom django.views.generic import ListView class PostListView(ListView):    model = Post    template_name = 'post_list.html'    context_object_name = 'posts' # urls.pypath('posts/', PostListView.as_view(), name='post_list') # Benefits:# - Less code# - Reusable# - Built-in functionality# - Mixins for extending# - Consistent patterns

Generic Views Available

# Display viewsfrom django.views.generic import (    TemplateView,      # Display template    ListView,          # List of objects    DetailView,        # Single object detail) # Edit viewsfrom django.views.generic.edit import (    FormView,          # Display and process form    CreateView,        # Create object    UpdateView,        # Update object    DeleteView,        # Delete object) # Date-based viewsfrom django.views.generic.dates import (    ArchiveIndexView,  # Archive index    YearArchiveView,   # Year archive    MonthArchiveView,  # Month archive    DayArchiveView,    # Day archive    DateDetailView,    # Date-based detail)

TemplateView

Basic TemplateView

# views.pyfrom django.views.generic import TemplateView class HomeView(TemplateView):    template_name = 'home.html' # urls.pyfrom django.urls import pathfrom .views import HomeView urlpatterns = [    path('', HomeView.as_view(), name='home'),] # Inline usage in urls.pyurlpatterns = [    path('about/', TemplateView.as_view(template_name='about.html'), name='about'),]

TemplateView with Context

class HomeView(TemplateView):    template_name = 'home.html'        def get_context_data(self, **kwargs):        context = super().get_context_data(**kwargs)        context['title'] = 'Welcome to My Site'        context['latest_posts'] = Post.objects.all()[:5]        context['stats'] = {            'total_posts': Post.objects.count(),            'total_users': User.objects.count(),        }        return context # template: home.html# {{ title }}# {% for post in latest_posts %}#     {{ post.title }}# {% endfor %}

TemplateView with URL Parameters

class PageView(TemplateView):    template_name = 'page.html'        def get_context_data(self, **kwargs):        context = super().get_context_data(**kwargs)        # Access URL parameter        page_slug = self.kwargs.get('slug')        context['page'] = Page.objects.get(slug=page_slug)        return context # urls.pypath('page/<slug:slug>/', PageView.as_view(), name='page_detail')

ListView

Basic ListView

from django.views.generic import ListViewfrom .models import Post class PostListView(ListView):    model = Post    template_name = 'blog/post_list.html'    context_object_name = 'posts'  # Default: object_list    # Template: blog/post_list.html{% for post in posts %}    <h2>{{ post.title }}</h2>    <p>{{ post.content }}</p>{% endfor %} # urls.pypath('posts/', PostListView.as_view(), name='post_list')

ListView with Filtering

class PublishedPostListView(ListView):    model = Post    template_name = 'blog/post_list.html'    context_object_name = 'posts'        def get_queryset(self):        # Custom queryset        return Post.objects.filter(            published=True        ).select_related('author').order_by('-created') # Filter by category from URLclass CategoryPostListView(ListView):    model = Post    template_name = 'blog/category_posts.html'    context_object_name = 'posts'        def get_queryset(self):        category_slug = self.kwargs.get('slug')        return Post.objects.filter(            category__slug=category_slug,            published=True        )        def get_context_data(self, **kwargs):        context = super().get_context_data(**kwargs)        category_slug = self.kwargs.get('slug')        context['category'] = Category.objects.get(slug=category_slug)        return context # urls.pypath('category/<slug:slug>/', CategoryPostListView.as_view(), name='category_posts')

ListView with Pagination

class PostListView(ListView):    model = Post    template_name = 'blog/post_list.html'    context_object_name = 'posts'    paginate_by = 10  # Items per page        def get_queryset(self):        return Post.objects.filter(published=True).order_by('-created') # Template: blog/post_list.html{% for post in posts %}    <h2>{{ post.title }}</h2>{% endfor %} <!-- Pagination --><div class="pagination">    {% if page_obj.has_previous %}        <a href="?page=1">First</a>        <a href="?page={{ page_obj.previous_page_number }}">Previous</a>    {% endif %}        <span>        Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}    </span>        {% if page_obj.has_next %}        <a href="?page={{ page_obj.next_page_number }}">Next</a>        <a href="?page={{ page_obj.paginator.num_pages }}">Last</a>    {% endif %}</div> # Custom paginate_by from GET parameterclass PostListView(ListView):    model = Post        def get_paginate_by(self, queryset):        return self.request.GET.get('per_page', 10)
from django.db.models import Q class PostSearchView(ListView):    model = Post    template_name = 'blog/post_search.html'    context_object_name = 'posts'    paginate_by = 20        def get_queryset(self):        query = self.request.GET.get('q', '')                if query:            return Post.objects.filter(                Q(title__icontains=query) |                Q(content__icontains=query) |                Q(author__username__icontains=query)            ).distinct()                return Post.objects.none()        def get_context_data(self, **kwargs):        context = super().get_context_data(**kwargs)        context['query'] = self.request.GET.get('q', '')        return context # Template: Search form<form method="get">    <input type="text" name="q" value="{{ query }}" placeholder="Search...">    <button type="submit">Search</button></form> {% for post in posts %}    <h2>{{ post.title }}</h2>{% endfor %}

DetailView

Basic DetailView

from django.views.generic import DetailView class PostDetailView(DetailView):    model = Post    template_name = 'blog/post_detail.html'    context_object_name = 'post'        # By default, looks for 'pk' or 'slug' in URL    # slug_field = 'slug'  # Model field name (default: 'slug')    # slug_url_kwarg = 'slug'  # URL parameter name (default: 'slug') # urls.pypath('post/<int:pk>/', PostDetailView.as_view(), name='post_detail')# Or with slug:path('post/<slug:slug>/', PostDetailView.as_view(), name='post_detail') # Template: blog/post_detail.html<h1>{{ post.title }}</h1><p>By {{ post.author }} on {{ post.created|date:"M d, Y" }}</p><div>{{ post.content|safe }}</div>
class PostDetailView(DetailView):    model = Post    template_name = 'blog/post_detail.html'        def get_queryset(self):        # Optimize queries        return Post.objects.select_related(            'author', 'category'        ).prefetch_related('tags', 'comments')        def get_context_data(self, **kwargs):        context = super().get_context_data(**kwargs)                # Add related objects        context['related_posts'] = Post.objects.filter(            category=self.object.category        ).exclude(pk=self.object.pk)[:5]                context['comments'] = self.object.comments.filter(            approved=True        ).order_by('-created')                return context

DetailView with Counter

class PostDetailView(DetailView):    model = Post    template_name = 'blog/post_detail.html'        def get_object(self):        obj = super().get_object()                # Increment view count        obj.views = F('views') + 1        obj.save(update_fields=['views'])                # Refresh to get actual value        obj.refresh_from_db()                return obj

CreateView

Basic CreateView

from django.views.generic.edit import CreateViewfrom django.urls import reverse_lazyfrom .models import Postfrom .forms import PostForm class PostCreateView(CreateView):    model = Post    form_class = PostForm    template_name = 'blog/post_form.html'    success_url = reverse_lazy('post_list')        # Or specify fields directly (auto-generates form)    # fields = ['title', 'content', 'category', 'tags'] # urls.pypath('post/create/', PostCreateView.as_view(), name='post_create') # Template: blog/post_form.html<form method="post">    {% csrf_token %}    {{ form.as_p }}    <button type="submit">Create Post</button></form>

CreateView with User Assignment

from django.contrib.auth.mixins import LoginRequiredMixin class PostCreateView(LoginRequiredMixin, CreateView):    model = Post    form_class = PostForm    template_name = 'blog/post_form.html'        def form_valid(self, form):        # Set author before saving        form.instance.author = self.request.user        return super().form_valid(form)        def get_success_url(self):        # Redirect to created post        return reverse_lazy('post_detail', kwargs={'pk': self.object.pk})

CreateView with Custom Logic

class PostCreateView(LoginRequiredMixin, CreateView):    model = Post    form_class = PostForm    template_name = 'blog/post_form.html'        def get_form_kwargs(self):        # Pass user to form        kwargs = super().get_form_kwargs()        kwargs['user'] = self.request.user        return kwargs        def form_valid(self, form):        # Custom processing before save        form.instance.author = self.request.user        form.instance.slug = slugify(form.instance.title)                # Save        response = super().form_valid(form)                # Custom processing after save        messages.success(self.request, 'Post created successfully!')                # Send notification        notify_followers(self.request.user, self.object)                return response        def form_invalid(self, form):        messages.error(self.request, 'Please correct the errors below.')        return super().form_invalid(form)

UpdateView

Basic UpdateView

from django.views.generic.edit import UpdateView class PostUpdateView(UpdateView):    model = Post    form_class = PostForm    template_name = 'blog/post_form.html'        def get_success_url(self):        return reverse_lazy('post_detail', kwargs={'pk': self.object.pk}) # urls.pypath('post/<int:pk>/edit/', PostUpdateView.as_view(), name='post_update')

UpdateView with Permission Check

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):    model = Post    form_class = PostForm    template_name = 'blog/post_form.html'        def test_func(self):        # Only author can edit        post = self.get_object()        return self.request.user == post.author        def get_success_url(self):        messages.success(self.request, 'Post updated successfully!')        return reverse_lazy('post_detail', kwargs={'pk': self.object.pk})

UpdateView with Partial Update

class PostUpdateView(LoginRequiredMixin, UpdateView):    model = Post    fields = ['title', 'content', 'category']  # Only these fields    template_name = 'blog/post_form.html'        def get_queryset(self):        # Only user's own posts        return Post.objects.filter(author=self.request.user)        def form_valid(self, form):        # Track modification        form.instance.modified_by = self.request.user        form.instance.modified_at = timezone.now()                return super().form_valid(form)

DeleteView

Basic DeleteView

from django.views.generic.edit import DeleteView class PostDeleteView(DeleteView):    model = Post    template_name = 'blog/post_confirm_delete.html'    success_url = reverse_lazy('post_list') # urls.pypath('post/<int:pk>/delete/', PostDeleteView.as_view(), name='post_delete') # Template: blog/post_confirm_delete.html<h2>Delete Post</h2><p>Are you sure you want to delete "{{ post.title }}"?</p><form method="post">    {% csrf_token %}    <button type="submit">Confirm Delete</button>    <a href="{% url 'post_detail' post.pk %}">Cancel</a></form>

DeleteView with Permission

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):    model = Post    template_name = 'blog/post_confirm_delete.html'    success_url = reverse_lazy('post_list')        def test_func(self):        post = self.get_object()        return self.request.user == post.author or self.request.user.is_staff        def delete(self, request, *args, **kwargs):        # Custom logic before delete        post = self.get_object()        post_title = post.title                # Perform delete        response = super().delete(request, *args, **kwargs)                # Custom logic after delete        messages.success(request, f'Post "{post_title}" deleted successfully.')                return response

Soft Delete

class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):    model = Post    template_name = 'blog/post_confirm_delete.html'    success_url = reverse_lazy('post_list')        def test_func(self):        post = self.get_object()        return self.request.user == post.author        def delete(self, request, *args, **kwargs):        # Soft delete instead of actual delete        self.object = self.get_object()        self.object.deleted = True        self.object.deleted_at = timezone.now()        self.object.save()                messages.success(request, 'Post deleted successfully.')        return redirect(self.success_url)

Mixins and Custom Views

Multiple Mixins

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixinfrom django.views.generic import DetailView class PostDetailView(LoginRequiredMixin, UserPassesTestMixin, DetailView):    model = Post    template_name = 'blog/post_detail.html'        def test_func(self):        post = self.get_object()        # Published posts or author's draft        return post.published or post.author == self.request.user # Mixin order matters: left to right

Custom Mixin

class AuthorRequiredMixin:    """Require user to be the author"""        def dispatch(self, request, *args, **kwargs):        obj = self.get_object()        if obj.author != request.user:            return HttpResponseForbidden("You don't have permission.")        return super().dispatch(request, *args, **kwargs) class PostUpdateView(LoginRequiredMixin, AuthorRequiredMixin, UpdateView):    model = Post    fields = ['title', 'content']    template_name = 'blog/post_form.html'

Form Mixin Example

class FormMessageMixin:    """Add success/error messages to form views"""        success_message = ''    error_message = ''        def form_valid(self, form):        if self.success_message:            messages.success(self.request, self.success_message)        return super().form_valid(form)        def form_invalid(self, form):        if self.error_message:            messages.error(self.request, self.error_message)        return super().form_invalid(form) class PostCreateView(FormMessageMixin, CreateView):    model = Post    form_class = PostForm    success_message = 'Post created successfully!'    error_message = 'Please correct the errors below.'

Bài Tập

Bài 1: Blog CRUD

Task: Implement complete blog CRUD:

# Models:# - Post (title, slug, content, author, category, published, created)# - Category (name, slug) # Requirements:# 1. PostListView:#    - Show all published posts#    - Paginate (10 per page)#    - Filter by category#    - Search by title/content # 2. PostDetailView:#    - Show post details#    - Show related posts (same category)#    - Increment view count # 3. PostCreateView:#    - Login required#    - Auto-set author#    - Auto-generate slug # 4. PostUpdateView:#    - Only author can edit#    - Show success message # 5. PostDeleteView:#    - Only author can delete#    - Confirmation page#    - Soft delete (set deleted=True)

Bài 2: User Dashboard

Task: Create user dashboard with generic views:

# Requirements:# 1. ProfileView (DetailView):#    - Show user profile#    - Show user's stats (post count, followers)#    - Show recent posts # 2. MyPostsListView (ListView):#    - Show current user's posts#    - Filter: all, published, drafts#    - Sort: newest, oldest, most viewed # 3. ProfileUpdateView (UpdateView):#    - Update profile info#    - Upload avatar#    - Validation # 4. PostsByUserListView (ListView):#    - Show posts by specific user#    - Public posts only#    - Pagination

Bài 3: Product Catalog

Task: E-commerce product catalog:

# Models:# - Product (name, slug, price, description, category, in_stock)# - Category (name, slug) # Requirements:# 1. ProductListView:#    - Grid layout#    - Filter by: category, price range, in_stock#    - Sort: price (asc/desc), name, newest#    - Search#    - Pagination # 2. ProductDetailView:#    - Product details#    - Related products#    - Add to cart button # 3. CategoryListView:#    - All categories with product count # 4. CategoryProductListView:#    - Products in category#    - Subcategories

Bài 4: Comment System

Task: Implement comment system:

# Model:# - Comment (post, author, content, parent, approved, created) # Requirements:# 1. CommentCreateView:#    - Add comment to post#    - Login required#    - Auto-approve if user is staff#    - Email notification to post author # 2. CommentUpdateView:#    - Edit own comment (within 5 minutes)#    - Show "edited" indicator # 3. CommentDeleteView:#    - Delete own comment#    - Or staff can delete any # 4. CommentListView:#    - Show all comments for user#    - Filter: approved, pending, by post # Bonus: Nested comments (replies)

Tài Liệu Tham Khảo


Previous: Bài 18: Django Settings | Next: Bài 20: Testing Django Applications