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.
from django.views.generic import CreateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
class PostCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
"""Basic post creation view"""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
success_url = reverse_lazy('blog:post_list')
success_message = "Post created successfully!"
def form_valid(self, form):
"""Set the author before saving"""
form.instance.author = self.request.user
return super().form_valid(form)
class CategoryCreateView(LoginRequiredMixin, CreateView):
"""Category creation with custom success URL"""
model = Category
fields = ['name', 'description', 'parent']
template_name = 'blog/category_form.html'
def get_success_url(self):
"""Redirect to the created category"""
return reverse('blog:category_detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
"""Add custom processing before save"""
# Auto-generate slug from name
form.instance.slug = slugify(form.instance.name)
# Set creator
form.instance.created_by = self.request.user
return super().form_valid(form)
class AjaxCreateView(CreateView):
"""AJAX-enabled create view"""
model = Post
form_class = PostForm
def form_valid(self, form):
"""Handle AJAX form submission"""
response = super().form_valid(form)
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'success': True,
'object_id': self.object.pk,
'redirect_url': self.get_success_url(),
'message': 'Post created successfully!'
})
return response
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
class AdvancedPostCreateView(LoginRequiredMixin, CreateView):
"""Advanced post creation with file handling"""
model = Post
form_class = PostForm
template_name = 'blog/post_create.html'
def get_form_kwargs(self):
"""Pass additional data to form"""
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def get_context_data(self, **kwargs):
"""Add extra context for template"""
context = super().get_context_data(**kwargs)
context.update({
'categories': Category.objects.filter(active=True),
'tags': Tag.objects.all(),
'user_drafts': Post.objects.filter(
author=self.request.user,
status='draft'
).count(),
})
return context
def form_valid(self, form):
"""Custom form processing"""
# Set author
form.instance.author = self.request.user
# Handle draft vs publish
if 'save_draft' in self.request.POST:
form.instance.status = 'draft'
elif 'publish' in self.request.POST:
form.instance.status = 'published'
form.instance.published_at = timezone.now()
# Process uploaded images
self.process_uploaded_files(form)
response = super().form_valid(form)
# Send notification if published
if form.instance.status == 'published':
self.send_publication_notification()
return response
def process_uploaded_files(self, form):
"""Process any uploaded files"""
uploaded_files = self.request.FILES.getlist('additional_files')
for uploaded_file in uploaded_files:
# Validate file
if self.validate_file(uploaded_file):
# Create attachment record
PostAttachment.objects.create(
post=form.instance,
file=uploaded_file,
uploaded_by=self.request.user
)
def validate_file(self, uploaded_file):
"""Validate uploaded file"""
# Check file size (10MB limit)
if uploaded_file.size > 10 * 1024 * 1024:
messages.error(self.request, f'File {uploaded_file.name} is too large')
return False
# Check file type
allowed_types = ['image/jpeg', 'image/png', 'application/pdf']
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
def send_publication_notification(self):
"""Send notification when post is published"""
# Notify subscribers
subscribers = User.objects.filter(
profile__subscribed_to_notifications=True
)
for subscriber in subscribers:
send_notification.delay(
user_id=subscriber.id,
message=f'New post published: {self.object.title}',
post_id=self.object.id
)
def get_success_url(self):
"""Dynamic success URL based on action"""
if 'save_draft' in self.request.POST:
messages.success(self.request, 'Post saved as draft')
return reverse('blog:post_edit', kwargs={'pk': self.object.pk})
else:
messages.success(self.request, 'Post published successfully!')
return self.object.get_absolute_url()
class BulkCreateView(LoginRequiredMixin, TemplateView):
"""Handle bulk object creation"""
template_name = 'blog/bulk_create.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Create multiple form instances
context['forms'] = [PostForm(prefix=str(i)) for i in range(5)]
return context
def post(self, request, *args, **kwargs):
"""Handle bulk form submission"""
forms = [PostForm(request.POST, prefix=str(i)) for i in range(5)]
valid_forms = []
invalid_forms = []
for form in forms:
if form.is_valid() and form.has_changed():
valid_forms.append(form)
elif form.has_changed():
invalid_forms.append(form)
if invalid_forms:
# Some forms have errors
context = self.get_context_data()
context['forms'] = forms
return self.render_to_response(context)
# Save valid forms
created_objects = []
for form in valid_forms:
obj = form.save(commit=False)
obj.author = request.user
obj.save()
form.save_m2m()
created_objects.append(obj)
messages.success(
request,
f'{len(created_objects)} posts created successfully!'
)
return redirect('blog:post_list')
class PostUpdateView(LoginRequiredMixin, UpdateView):
"""Basic post update view"""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def get_queryset(self):
"""Only allow editing own posts"""
return Post.objects.filter(author=self.request.user)
def get_success_url(self):
return self.object.get_absolute_url()
class StaffPostUpdateView(LoginRequiredMixin, UpdateView):
"""Staff can edit any post"""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def dispatch(self, request, *args, **kwargs):
"""Check permissions"""
if not request.user.is_staff:
raise PermissionDenied("Only staff can edit posts")
return super().dispatch(request, *args, **kwargs)
class ConditionalUpdateView(LoginRequiredMixin, UpdateView):
"""Update with conditional permissions"""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def get_object(self, queryset=None):
"""Get object with permission check"""
obj = super().get_object(queryset)
# Check if user can edit this post
if not self.can_edit_post(obj):
raise PermissionDenied("You don't have permission to edit this post")
return obj
def can_edit_post(self, post):
"""Check if user can edit the post"""
user = self.request.user
# Author can always edit
if post.author == user:
return True
# Staff can edit any post
if user.is_staff:
return True
# Editors can edit posts in their categories
if hasattr(user, 'profile') and user.profile.is_editor:
return post.category in user.profile.editable_categories.all()
return False
class AdvancedPostUpdateView(LoginRequiredMixin, UpdateView):
"""Advanced update with version control and audit trail"""
model = Post
form_class = PostForm
template_name = 'blog/post_update.html'
def get_object(self, queryset=None):
"""Get object and create version backup"""
obj = super().get_object(queryset)
# Create version backup before editing
self.create_version_backup(obj)
return obj
def create_version_backup(self, obj):
"""Create a backup version of the object"""
PostVersion.objects.create(
post=obj,
title=obj.title,
content=obj.content,
status=obj.status,
version_created_by=self.request.user,
version_created_at=timezone.now()
)
def get_context_data(self, **kwargs):
"""Add version history to context"""
context = super().get_context_data(**kwargs)
context.update({
'versions': PostVersion.objects.filter(
post=self.object
).order_by('-version_created_at')[:10],
'can_delete': self.can_delete_post(),
'edit_history': self.get_edit_history(),
})
return context
def can_delete_post(self):
"""Check if user can delete this post"""
return (
self.object.author == self.request.user or
self.request.user.is_staff
)
def get_edit_history(self):
"""Get edit history for this post"""
return PostEditLog.objects.filter(
post=self.object
).select_related('user').order_by('-timestamp')[:20]
def form_valid(self, form):
"""Handle form validation with change tracking"""
# Track changes
changes = self.track_changes(form)
# Handle status changes
old_status = self.object.status
new_status = form.cleaned_data.get('status')
if old_status != new_status:
self.handle_status_change(old_status, new_status)
response = super().form_valid(form)
# Log the edit
self.log_edit(changes)
return response
def track_changes(self, form):
"""Track what fields were changed"""
changes = {}
for field_name, field in form.fields.items():
old_value = getattr(self.object, field_name, None)
new_value = form.cleaned_data.get(field_name)
if old_value != new_value:
changes[field_name] = {
'old': old_value,
'new': new_value
}
return changes
def handle_status_change(self, old_status, new_status):
"""Handle post status changes"""
if old_status == 'draft' and new_status == 'published':
# Publishing a draft
self.object.published_at = timezone.now()
self.send_publication_notification()
elif old_status == 'published' and new_status == 'draft':
# Unpublishing a post
self.object.published_at = None
self.send_unpublication_notification()
def log_edit(self, changes):
"""Log the edit action"""
PostEditLog.objects.create(
post=self.object,
user=self.request.user,
changes=changes,
timestamp=timezone.now(),
ip_address=self.get_client_ip()
)
def get_client_ip(self):
"""Get client IP address"""
x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0]
return self.request.META.get('REMOTE_ADDR')
class PartialUpdateView(UpdateView):
"""Handle partial updates (PATCH-like behavior)"""
model = Post
fields = ['title', 'content', 'status']
def get_form_kwargs(self):
"""Only include changed fields in form"""
kwargs = super().get_form_kwargs()
# For PATCH requests, only update provided fields
if self.request.method == 'PATCH':
import json
try:
patch_data = json.loads(self.request.body)
# Only include fields that are being updated
kwargs['data'] = {
field: value for field, value in patch_data.items()
if field in self.fields
}
except json.JSONDecodeError:
pass
return kwargs
def form_valid(self, form):
"""Save only changed fields"""
if self.request.method == 'PATCH':
# Only save fields that were actually changed
changed_fields = []
for field_name in form.changed_data:
if field_name in self.fields:
setattr(self.object, field_name, form.cleaned_data[field_name])
changed_fields.append(field_name)
if changed_fields:
self.object.save(update_fields=changed_fields)
return JsonResponse({
'success': True,
'updated_fields': changed_fields,
'object_id': self.object.pk
})
return super().form_valid(form)
class PostDeleteView(LoginRequiredMixin, DeleteView):
"""Basic post deletion view"""
model = Post
template_name = 'blog/post_confirm_delete.html'
success_url = reverse_lazy('blog:post_list')
def get_queryset(self):
"""Only allow deleting own posts"""
return Post.objects.filter(author=self.request.user)
def delete(self, request, *args, **kwargs):
"""Add success message"""
messages.success(request, 'Post deleted successfully!')
return super().delete(request, *args, **kwargs)
class SafeDeleteView(LoginRequiredMixin, DeleteView):
"""Soft delete implementation"""
model = Post
template_name = 'blog/post_confirm_delete.html'
success_url = reverse_lazy('blog:post_list')
def delete(self, request, *args, **kwargs):
"""Soft delete instead of hard delete"""
self.object = self.get_object()
# Mark as deleted instead of actually deleting
self.object.deleted = True
self.object.deleted_at = timezone.now()
self.object.deleted_by = request.user
self.object.save()
messages.success(request, 'Post moved to trash')
return HttpResponseRedirect(self.get_success_url())
class ConditionalDeleteView(LoginRequiredMixin, DeleteView):
"""Delete with conditions and confirmations"""
model = Post
template_name = 'blog/post_confirm_delete.html'
def get_object(self, queryset=None):
"""Get object with permission check"""
obj = super().get_object(queryset)
if not self.can_delete_post(obj):
raise PermissionDenied("You don't have permission to delete this post")
return obj
def can_delete_post(self, post):
"""Check if post can be deleted"""
user = self.request.user
# Author can delete their own posts
if post.author == user:
return True
# Staff can delete any post
if user.is_staff:
return True
# Can't delete published posts with comments
if post.status == 'published' and post.comments.exists():
return False
return False
def get_context_data(self, **kwargs):
"""Add deletion impact information"""
context = super().get_context_data(**kwargs)
context.update({
'comment_count': self.object.comments.count(),
'has_attachments': self.object.attachments.exists(),
'related_posts': Post.objects.filter(
related_posts=self.object
).count(),
'deletion_warnings': self.get_deletion_warnings(),
})
return context
def get_deletion_warnings(self):
"""Get warnings about deletion consequences"""
warnings = []
if self.object.comments.exists():
warnings.append(f'This post has {self.object.comments.count()} comments that will be deleted')
if self.object.attachments.exists():
warnings.append(f'This post has {self.object.attachments.count()} file attachments that will be deleted')
if self.object.featured:
warnings.append('This is a featured post')
return warnings
class AdvancedDeleteView(LoginRequiredMixin, DeleteView):
"""Advanced deletion with backup and recovery"""
model = Post
template_name = 'blog/post_confirm_delete.html'
def delete(self, request, *args, **kwargs):
"""Delete with backup and cleanup"""
self.object = self.get_object()
# Create backup before deletion
backup_data = self.create_backup()
# Clean up related objects
self.cleanup_related_objects()
# Log deletion
self.log_deletion()
# Perform actual deletion
response = super().delete(request, *args, **kwargs)
# Send notification
self.send_deletion_notification(backup_data)
return response
def create_backup(self):
"""Create backup of object before deletion"""
backup_data = {
'id': self.object.id,
'title': self.object.title,
'content': self.object.content,
'author_id': self.object.author.id,
'created_at': self.object.created_at,
'deleted_at': timezone.now(),
'deleted_by': self.request.user.id,
}
# Store backup
PostBackup.objects.create(
original_id=self.object.id,
backup_data=backup_data,
created_by=self.request.user
)
return backup_data
def cleanup_related_objects(self):
"""Clean up related objects"""
# Delete comments
self.object.comments.all().delete()
# Delete attachments and files
for attachment in self.object.attachments.all():
if attachment.file:
attachment.file.delete()
attachment.delete()
# Remove from favorites
self.object.favorited_by.clear()
# Update related posts
related_posts = Post.objects.filter(related_posts=self.object)
for post in related_posts:
post.related_posts.remove(self.object)
def log_deletion(self):
"""Log the deletion action"""
DeletionLog.objects.create(
object_type='Post',
object_id=self.object.id,
object_title=self.object.title,
deleted_by=self.request.user,
deleted_at=timezone.now(),
ip_address=self.get_client_ip(),
reason=self.request.POST.get('deletion_reason', '')
)
def send_deletion_notification(self, backup_data):
"""Send notification about deletion"""
# Notify author if different from deleter
if self.object.author != self.request.user:
send_notification.delay(
user_id=self.object.author.id,
message=f'Your post "{self.object.title}" was deleted',
notification_type='post_deleted'
)
# Notify administrators
admin_users = User.objects.filter(is_staff=True)
for admin in admin_users:
send_notification.delay(
user_id=admin.id,
message=f'Post "{self.object.title}" was deleted by {self.request.user.username}',
notification_type='admin_post_deleted'
)
class BulkDeleteView(LoginRequiredMixin, TemplateView):
"""Handle bulk deletion of objects"""
template_name = 'blog/bulk_delete.html'
def post(self, request, *args, **kwargs):
"""Handle bulk deletion"""
object_ids = request.POST.getlist('object_ids')
if not object_ids:
messages.error(request, 'No objects selected for deletion')
return redirect('blog:post_list')
# Get objects user can delete
queryset = Post.objects.filter(
id__in=object_ids,
author=request.user
)
if not queryset.exists():
messages.error(request, 'No valid objects found for deletion')
return redirect('blog:post_list')
# Confirm deletion
if 'confirm' in request.POST:
count = queryset.count()
# Log bulk deletion
BulkDeletionLog.objects.create(
user=request.user,
object_type='Post',
object_count=count,
object_ids=object_ids,
timestamp=timezone.now()
)
# Delete objects
queryset.delete()
messages.success(request, f'{count} posts deleted successfully')
return redirect('blog:post_list')
# Show confirmation page
context = {
'objects': queryset,
'object_count': queryset.count(),
}
return self.render_to_response(context)
class PostCRUDMixin:
"""Common functionality for post CRUD operations"""
model = Post
def get_queryset(self):
"""Base queryset with optimizations"""
return Post.objects.select_related('author', 'category').prefetch_related('tags')
def get_context_data(self, **kwargs):
"""Common context data"""
context = super().get_context_data(**kwargs)
context.update({
'categories': Category.objects.filter(active=True),
'user_post_count': Post.objects.filter(author=self.request.user).count(),
})
return context
class PostListView(PostCRUDMixin, ListView):
"""List all posts"""
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 15
class PostDetailView(PostCRUDMixin, DetailView):
"""View single post"""
template_name = 'blog/post_detail.html'
context_object_name = 'post'
class PostCreateView(LoginRequiredMixin, PostCRUDMixin, CreateView):
"""Create new post"""
form_class = PostForm
template_name = 'blog/post_form.html'
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class PostUpdateView(LoginRequiredMixin, PostCRUDMixin, UpdateView):
"""Update existing post"""
form_class = PostForm
template_name = 'blog/post_form.html'
def get_queryset(self):
return super().get_queryset().filter(author=self.request.user)
class PostDeleteView(LoginRequiredMixin, PostCRUDMixin, DeleteView):
"""Delete post"""
template_name = 'blog/post_confirm_delete.html'
success_url = reverse_lazy('blog:post_list')
def get_queryset(self):
return super().get_queryset().filter(author=self.request.user)
# URL patterns
urlpatterns = [
path('', PostListView.as_view(), name='post_list'),
path('<int:pk>/', PostDetailView.as_view(), name='post_detail'),
path('create/', PostCreateView.as_view(), name='post_create'),
path('<int:pk>/edit/', PostUpdateView.as_view(), name='post_edit'),
path('<int:pk>/delete/', PostDeleteView.as_view(), name='post_delete'),
]
CRUD views provide the foundation for most web application functionality. Django's generic views handle the common patterns while allowing extensive customization for specific requirements. Understanding these patterns enables rapid development of robust, maintainable applications with proper security, validation, and user experience considerations.
Built-in Generic Views
Django's built-in generic views provide powerful, reusable patterns for common web application tasks. These views handle the most frequent operations like displaying lists of objects, showing object details, and managing object lifecycle operations.
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.