ModelAdmin, inline, actions, custom pages, admin sites
Django Admin — мощный инструмент для управления данными. Этот туториал покажет как кастомизировать админку под ваши нужды.
💡 Правило: Django Admin — не только для прототипов. Правильно настроенная админка экономит часы разработки интерфейсов управления.
# admin.py
from django.contrib import admin
from blog.models import Post, Category, Tag
# Декоратор (рекомендуется)
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'author', 'created_at', 'is_published']
list_filter = ['is_published', 'category', 'created_at']
search_fields = ['title', 'content']
# Или через admin.site.register()
admin.site.register(Category)
admin.site.register(Tag)@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = [
'title',
'author',
'category',
'created_at',
'is_published',
'view_count',
'published_status', # Custom method
]
# Сортировка по колонкам
list_display_links = ['title', 'author'] # Кликабельные поля
# Поля которые можно редактировать прямо в списке
list_editable = ['is_published', 'category']
# Метод для custom колонки
def published_status(self, obj):
if obj.is_published:
return '✓ Published'
return '✗ Draft'
published_status.short_description = 'Status'
published_status.admin_order_field = 'is_published' # Сортировка@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_filter = [
'is_published', # Boolean фильтр
'category', # ForeignKey фильтр
'author', # ForeignKey фильтр
'created_at', # Date фильтр (иерархический)
'tags', # ManyToMany фильтр
('status', admin.SimpleListFilter), # Custom фильтр
]
# Custom фильтр
class StatusFilter(admin.SimpleListFilter):
title = 'status'
parameter_name = 'status'
def lookups(self, request, model_admin):
return (
('draft', 'Draft'),
('pending', 'Pending Review'),
('published', 'Published'),
)
def queryset(self, request, queryset):
if self.value() == 'draft':
return queryset.filter(status='draft', is_published=False)
if self.value() == 'pending':
return queryset.filter(status='pending')
if self.value() == 'published':
return queryset.filter(is_published=True)
return queryset@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
search_fields = [
'title', # Поиск по заголовку
'content', # Поиск по содержанию
'author__username', # Поиск по username автора
'author__email', # Поиск по email автора
'category__name', # Поиск по названию категории
'^title', # Starts with (только начало)
'=title', # Exact match
'@content', # Full-text search (MySQL/PostgreSQL)
]@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
date_hierarchy = 'created_at' # Навигация: Год → Месяц → День@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('title',)}
# slug автоматически заполняется из title при вводеfrom blog.models import Post, Comment
# Комментарии в виде таблицы (компактно)
class CommentInline(admin.TabularInline):
model = Comment
extra = 1 # Количество пустых форм
fields = ['author', 'content', 'is_approved', 'created_at']
readonly_fields = ['created_at']
show_change_link = True # Ссылка на редактирование
# Комментарии в виде вертикальных блоков
class CommentInlineStacked(admin.StackedInline):
model = Comment
extra = 0
fields = ['author', 'content', 'is_approved']
classes = ['collapse'] # Сворачиваемый блок
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
inlines = [CommentInline]class ImageInline(admin.TabularInline):
model = PostImage
extra = 1
fields = ['image', 'caption', 'is_primary']
def formfield_for_foreignkey(self, db_field, request, **kwargs):
# Ограничить выбор изображений текущим пользователем
if db_field.name == 'image':
kwargs['queryset'] = Image.objects.filter(author=request.user)
return super().formfield_for_foreignkey(db_field, request, **kwargs)@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
actions = ['make_published', 'make_unpublished', 'feature_posts']
@admin.action(description='Publish selected posts')
def make_published(self, request, queryset):
updated = queryset.update(is_published=True, status='published')
self.message_user(
request,
f'{updated} posts were successfully published.',
level=messages.SUCCESS
)
@admin.action(description='Unpublish selected posts')
def make_unpublished(self, request, queryset):
updated = queryset.update(is_published=False)
self.message_user(
request,
f'{updated} posts were unpublished.',
level=messages.WARNING
)from django import forms
from django.core.exceptions import ValidationError
class EmailForm(forms.Form):
subject = forms.CharField(max_length=200, required=True)
message = forms.CharField(widget=forms.Textarea, required=True)
def clean_message(self):
msg = self.cleaned_data['message']
if len(msg) < 10:
raise ValidationError('Message must be at least 10 characters')
return msg
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
actions = ['email_users']
def email_users(self, request, queryset):
if request.POST.get('apply'):
form = EmailForm(request.POST)
if form.is_valid():
for user in queryset:
send_mail(
subject=form.cleaned_data['subject'],
message=form.cleaned_data['message'],
from_email='admin@example.com',
recipient_list=[user.email],
)
self.message_user(request, 'Emails sent successfully!')
return None
else:
form = EmailForm()
context = {
'form': form,
'action': 'email_users',
'users': queryset,
}
return render(request, 'admin/email_users.html', context)@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
fieldsets = [
('Basic Information', {
'fields': ['title', 'slug', 'author', 'category'],
'description': 'Basic post information'
}),
('Content', {
'fields': ['content', 'excerpt'],
'classes': ['collapse'] # Сворачиваемая секция
}),
('Publishing', {
'fields': ['status', 'is_published', 'published_at'],
'classes': ['collapse', 'wide']
}),
('Advanced', {
'fields': ['tags', 'views'],
'classes': ['collapse']
}),
]@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
readonly_fields = [
'created_at',
'updated_at',
'published_at',
'view_count',
'get_author_email',
]
def get_author_email(self, obj):
return obj.author.email
get_author_email.short_description = 'Author Email'@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super().get_queryset(request)
# Обычные пользователи видят только свои посты
if not request.user.is_superuser:
return qs.filter(author=request.user)
return qs
def has_add_permission(self, request):
# Запретить создание если больше 10 постов
if not request.user.is_superuser:
count = Post.objects.filter(author=request.user).count()
if count >= 10:
return False
return True
def has_change_permission(self, request, obj=None):
# Разрешить редактирование только своих постов
if obj and not request.user.is_superuser:
return obj.author == request.user
return super().has_change_permission(request, obj)
def has_delete_permission(self, request, obj=None):
# Разрешить удаление только своих постов
if obj and not request.user.is_superuser:
return obj.author == request.user
return super().has_delete_permission(request, obj)from django.urls import path
from django.template.response import TemplateResponse
from django.contrib.admin.views.decorators import staff_member_required
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path(
'stats/',
self.admin_site.admin_view(self.stats_view),
name='blog_post_stats',
),
]
return custom_urls + urls
@staff_member_required
def stats_view(self, request):
context = {
'total_posts': Post.objects.count(),
'published_posts': Post.objects.filter(is_published=True).count(),
'total_views': Post.objects.aggregate(total=Sum('views'))['total'],
'top_authors': User.objects.annotate(
post_count=Count('posts')
).order_by('-post_count')[:5],
}
return TemplateResponse(request, 'admin/blog/post_stats.html', context)<!-- templates/admin/blog/post_stats.html -->
{% extends "admin/base_site.html" %}
{% block content %}
<h1>Post Statistics</h1>
<div class="module">
<h2>Overview</h2>
<p>Total Posts: {{ total_posts }}</p>
<p>Published Posts: {{ published_posts }}</p>
<p>Total Views: {{ total_views }}</p>
</div>
<div class="module">
<h2>Top Authors</h2>
<ul>
{% for author in top_authors %}
<li>{{ author.username }}: {{ author.post_count }} posts</li>
{% endfor %}
</ul>
</div>
{% endblock %}# admin.py
from django.contrib.admin import AdminSite
from django.contrib.auth.models import User, Group
from django.contrib import admin
class CustomAdminSite(AdminSite):
site_header = 'My Company Admin'
site_title = 'My Company Admin Portal'
index_title = 'Welcome to My Company Admin'
def has_permission(self, request):
# Только для staff и active пользователей
return request.user.is_active and request.user.is_staff
# Создание экземпляра
custom_admin_site = CustomAdminSite(name='customadmin')
# Регистрация моделей
custom_admin_site.register(User)
custom_admin_site.register(Group)
custom_admin_site.register(Post, PostAdmin)# urls.py
from django.urls import path
from .admin import custom_admin_site
urlpatterns = [
path('custom-admin/', custom_admin_site.urls),
path('admin/', admin.site.urls), # Стандартная админка тоже доступна
]# blog/admin.py
from django.contrib import admin, messages
from django.urls import path
from django.template.response import TemplateResponse
from django.utils.html import format_html
from django.db.models import Count, Sum
from .models import Post, Category, Tag, Comment
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'post_count']
search_fields = ['name']
prepopulated_fields = {'slug': ('name',)}
def post_count(self, obj):
return obj.posts.count()
post_count.short_description = 'Posts'
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'post_count']
search_fields = ['name']
prepopulated_fields = {'slug': ('name',)}
def post_count(self, obj):
return obj.posts.count()
post_count.short_description = 'Posts'
class CommentInline(admin.TabularInline):
model = Comment
extra = 0
fields = ['author', 'content', 'is_approved', 'created_at']
readonly_fields = ['created_at']
show_change_link = True
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = [
'title',
'author',
'category',
'created_at',
'is_published',
'view_count',
'comment_count',
]
list_display_links = ['title']
list_editable = ['is_published']
list_filter = ['is_published', 'category', 'created_at', 'tags']
search_fields = ['title', 'content', 'author__username']
date_hierarchy = 'created_at'
prepopulated_fields = {'slug': ('title',)}
inlines = [CommentInline]
readonly_fields = ['created_at', 'updated_at', 'view_count']
fieldsets = [
('Basic Information', {
'fields': ['title', 'slug', 'author', 'category'],
}),
('Content', {
'fields': ['content', 'excerpt'],
'classes': ['collapse']
}),
('Publishing', {
'fields': ['status', 'is_published', 'tags'],
'classes': ['collapse']
}),
('Statistics', {
'fields': ['view_count', 'created_at', 'updated_at'],
'classes': ['collapse']
}),
]
actions = ['make_published', 'make_unpublished', 'feature_posts']
@admin.action(description='Publish selected posts')
def make_published(self, request, queryset):
updated = queryset.update(is_published=True, status='published')
self.message_user(request, f'{updated} posts published.', messages.SUCCESS)
@admin.action(description='Unpublish selected posts')
def make_unpublished(self, request, queryset):
updated = queryset.update(is_published=False)
self.message_user(request, f'{updated} posts unpublished.', messages.WARNING)
@admin.action(description='Feature selected posts')
def feature_posts(self, request, queryset):
updated = queryset.update(is_featured=True)
self.message_user(request, f'{updated} posts featured.', messages.SUCCESS)
def comment_count(self, obj):
return obj.comments.count()
comment_count.short_description = 'Comments'
def get_queryset(self, request):
qs = super().get_queryset(request)
if not request.user.is_superuser:
return qs.filter(author=request.user)
return qs
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path(
'stats/',
self.admin_site.admin_view(self.stats_view),
name='blog_post_stats',
),
]
return custom_urls + urls
def stats_view(self, request):
context = {
'total_posts': Post.objects.count(),
'published_posts': Post.objects.filter(is_published=True).count(),
'total_views': Post.objects.aggregate(total=Sum('views'))['total'] or 0,
'posts_by_category': Category.objects.annotate(
post_count=Count('posts')
).order_by('-post_count'),
'top_posts': Post.objects.annotate(
comment_count=Count('comments')
).order_by('-comment_count', '-views')[:10],
}
return TemplateResponse(request, 'admin/blog/post_stats.html', context)
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = ['post', 'author', 'content_preview', 'is_approved', 'created_at']
list_filter = ['is_approved', 'created_at']
search_fields = ['content', 'author__username', 'post__title']
date_hierarchy = 'created_at'
readonly_fields = ['created_at']
actions = ['approve_comments', 'delete_spam']
def content_preview(self, obj):
return obj.content[:50] + '...' if len(obj.content) > 50 else obj.content
content_preview.short_description = 'Content'
@admin.action(description='Approve selected comments')
def approve_comments(self, request, queryset):
updated = queryset.update(is_approved=True)
self.message_user(request, f'{updated} comments approved.', messages.SUCCESS)
@admin.action(description='Delete spam comments')
def delete_spam(self, request, queryset):
deleted = queryset.delete()
self.message_user(request, f'{deleted[0]} comments deleted.', messages.ERROR)Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.