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 patternsGeneric 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)ListView with Search
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>DetailView with Related Objects
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 contextDetailView 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 objCreateView
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 responseSoft 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 rightCustom 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# - PaginationBà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# - SubcategoriesBà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