Django

⌘K
  1. Home
  2. Django
  3. Speed Optimization
  4. Part 2: Admin Panel Optimization

Part 2: Admin Panel Optimization

Part 2: Admin Panel Optimization

Admin panel এ সবচেয়ে বেশি performance issue হয় কারণ:

  1. List page এ অনেক data দেখায়
  2. ForeignKey fields এ N+1 query problem
  3. ManyToMany relations slow
  4. Filtering ও searching optimize না থাকলে

🔴 Normal Admin (Slow)

shop/admin.py – Normal Version:

from django.contrib import admin
from .models import Category, Brand, Product, Review, Order, OrderItem

# ❌ সবচেয়ে সাধারণ (এবং ধীর) admin
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    pass  # কোন optimization নেই


@admin.register(Brand)
class BrandAdmin(admin.ModelAdmin):
    list_display = ['name', 'country']
    # ❌ সমস্যা: search optimize নেই


@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ['name', 'get_category', 'get_brand', 'price', 'stock']
    list_filter = ['is_active', 'category', 'brand']
    search_fields = ['name', 'description']

    # ❌ সমস্যা: প্রতিটি product এর জন্য আলাদা query
    def get_category(self, obj):
        return obj.category.name  # Extra query!
    get_category.short_description = 'Category'

    def get_brand(self, obj):
        return obj.brand.name  # Extra query!
    get_brand.short_description = 'Brand'

    # ❌ সমস্যা: queryset optimize নেই
    # 100 products = 1 (main) + 100 (category) + 100 (brand) = 201 queries!


@admin.register(Review)
class ReviewAdmin(admin.ModelAdmin):
    list_display = ['get_product', 'get_user', 'rating', 'created_at']

    def get_product(self, obj):
        return obj.product.name  # Extra query!

    def get_user(self, obj):
        return obj.user.username  # Extra query!


@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ['id', 'get_user', 'total_amount', 'status', 'created_at']

    def get_user(self, obj):
        return obj.user.username  # Extra query!


@admin.register(OrderItem)
class OrderItemAdmin(admin.ModelAdmin):
    list_display = ['get_order', 'get_product', 'quantity', 'price']

    def get_order(self, obj):
        return f"Order #{obj.order.id}"  # Extra query!

    def get_product(self, obj):
        return obj.product.name  # Extra query!

❌ Normal Admin এর সমস্যা:

Performance Test করুন:

# Django shell খুলুন
python manage.py shell
from django.contrib.admin.sites import site
from shop.models import Product
from django.test.utils import override_settings
from django.db import connection
from django.db import reset_queries

# Query count check
from django.conf import settings
settings.DEBUG = True

# Admin changelist query
from shop.admin import ProductAdmin
admin_instance = ProductAdmin(Product, site)

# Simulate admin list view
reset_queries()
queryset = admin_instance.get_queryset(request=None)
list(queryset[:100])  # First 100 products

print(f"Total Queries: {len(connection.queries)}")
# Output: 201 queries! (1 main + 100 category + 100 brand)

✅ Optimized Admin (Fast)

shop/admin.py – Optimized Version:

from django.contrib import admin
from django.db.models import Count, Avg, Sum, Q
from django.utils.html import format_html
from django.urls import reverse
from django.utils.safestring import mark_safe
from .models import Category, Brand, Product, Review, Order, OrderItem


# ✅ Category Admin - Optimized
@admin.register(Category)
class CategoryAdminOptimized(admin.ModelAdmin):
    list_display = [
        'name', 
        'slug', 
        'product_count',  # ✅ Annotated field
        'created_at'
    ]
    list_filter = ['created_at']
    search_fields = ['name', 'slug']  # ✅ Indexed fields এ search
    prepopulated_fields = {'slug': ('name',)}  # ✅ Auto slug
    date_hierarchy = 'created_at'  # ✅ Date navigation

    # ✅ Queryset optimization
    def get_queryset(self, request):
        queryset = super().get_queryset(request)
        # Annotate করে product count add করুন
        queryset = queryset.annotate(
            _product_count=Count('products', filter=Q(products__is_active=True))
        )
        return queryset

    # ✅ Annotated field display
    def product_count(self, obj):
        return obj._product_count
    product_count.short_description = 'Active Products'
    product_count.admin_order_field = '_product_count'  # ✅ Sortable


# ✅ Brand Admin - Optimized
@admin.register(Brand)
class BrandAdminOptimized(admin.ModelAdmin):
    list_display = [
        'name', 
        'slug', 
        'country', 
        'is_active',
        'product_count',
        'logo_preview'  # ✅ Image preview
    ]
    list_filter = ['is_active', 'country', 'created_at']
    search_fields = ['name', 'slug', 'country']
    prepopulated_fields = {'slug': ('name',)}
    list_editable = ['is_active']  # ✅ Quick edit

    def get_queryset(self, request):
        queryset = super().get_queryset(request)
        queryset = queryset.annotate(
            _product_count=Count('products')
        )
        return queryset

    def product_count(self, obj):
        return obj._product_count
    product_count.short_description = 'Products'
    product_count.admin_order_field = '_product_count'

    # ✅ Image preview
    def logo_preview(self, obj):
        if obj.logo:
            return format_html(
                '<img src="{}" width="50" height="50" style="object-fit:cover;border-radius:5px;" />',
                obj.logo.url
            )
        return '-'
    logo_preview.short_description = 'Logo'


# ✅ Product Admin - Fully Optimized
@admin.register(Product)
class ProductAdminOptimized(admin.ModelAdmin):
    list_display = [
        'name',
        'category_link',  # ✅ Clickable link
        'brand_link',     # ✅ Clickable link
        'price_display',  # ✅ Formatted price
        'stock_status',   # ✅ Visual stock indicator
        'rating_display', # ✅ Star rating
        'review_count',
        'is_active',
        'is_featured',
    ]

    list_filter = [
        'is_active',
        'is_featured',
        'category',
        'brand',
        'created_at',
        ('price', admin.NumericRangeFilter),  # ✅ Price range filter
    ]

    search_fields = [
        'name',
        'slug',
        'description',
        'category__name',  # ✅ Related field search
        'brand__name',
    ]

    list_editable = ['is_active', 'is_featured']  # ✅ Quick edit
    prepopulated_fields = {'slug': ('name',)}
    date_hierarchy = 'created_at'

    # ✅ Fieldsets for better organization
    fieldsets = (
        ('Basic Information', {
            'fields': ('name', 'slug', 'description')
        }),
        ('Pricing & Stock', {
            'fields': ('price', 'stock'),
            'classes': ('collapse',)  # ✅ Collapsible
        }),
        ('Relations', {
            'fields': ('category', 'brand')
        }),
        ('Status', {
            'fields': ('is_active', 'is_featured')
        }),
        ('Statistics (Read-only)', {
            'fields': ('review_count', 'average_rating'),
            'classes': ('collapse',),
        }),
    )

    readonly_fields = ['review_count', 'average_rating', 'created_at', 'updated_at']

    # ✅✅✅ সবচেয়ে গুরুত্বপূর্ণ: Queryset Optimization
    def get_queryset(self, request):
        queryset = super().get_queryset(request)

        # ✅ select_related: ForeignKey relations (1-to-1, Many-to-1)
        queryset = queryset.select_related(
            'category',  # Product -> Category
            'brand',     # Product -> Brand
        )

        # ✅ prefetch_related: Reverse ForeignKey & ManyToMany
        # (এখানে দরকার নেই কারণ list view এ reviews দেখাচ্ছি না)

        # ✅ only(): শুধু প্রয়োজনীয় fields load করুন
        # queryset = queryset.only(
        #     'id', 'name', 'price', 'stock', 'is_active',
        #     'category__name', 'brand__name'
        # )

        return queryset

    # ✅ Custom display methods (no extra queries!)
    def category_link(self, obj):
        """Category name with admin link"""
        url = reverse('admin:shop_category_change', args=[obj.category.id])
        return format_html('<a href="{}">{}</a>', url, obj.category.name)
    category_link.short_description = 'Category'
    category_link.admin_order_field = 'category__name'  # ✅ Sortable

    def brand_link(self, obj):
        """Brand name with admin link"""
        url = reverse('admin:shop_brand_change', args=[obj.brand.id])
        return format_html('<a href="{}">{}</a>', url, obj.brand.name)
    brand_link.short_description = 'Brand'
    brand_link.admin_order_field = 'brand__name'

    def price_display(self, obj):
        """Formatted price with currency"""
        return format_html('<strong>৳{:,.2f}</strong>', obj.price)
    price_display.short_description = 'Price'
    price_display.admin_order_field = 'price'

    def stock_status(self, obj):
        """Visual stock indicator"""
        if obj.stock > 50:
            color = 'green'
            status = f'✓ {obj.stock}'
        elif obj.stock > 0:
            color = 'orange'
            status = f'⚠ {obj.stock}'
        else:
            color = 'red'
            status = '✗ Out of Stock'

        return format_html(
            '<span style="color:{}; font-weight:bold;">{}</span>',
            color, status
        )
    stock_status.short_description = 'Stock'
    stock_status.admin_order_field = 'stock'

    def rating_display(self, obj):
        """Star rating display"""
        if obj.average_rating > 0:
            stars = '' * int(obj.average_rating)
            return format_html(
                '{} <small>({:.1f})</small>',
                stars, obj.average_rating
            )
        return '-'
    rating_display.short_description = 'Rating'
    rating_display.admin_order_field = 'average_rating'

    # ✅ Custom actions
    actions = ['make_active', 'make_inactive', 'make_featured', 'update_ratings']

    def make_active(self, request, queryset):
        updated = queryset.update(is_active=True)
        self.message_user(request, f'{updated} products marked as active.')
    make_active.short_description = 'Mark selected as Active'

    def make_inactive(self, request, queryset):
        updated = queryset.update(is_active=False)
        self.message_user(request, f'{updated} products marked as inactive.')
    make_inactive.short_description = 'Mark selected as Inactive'

    def make_featured(self, request, queryset):
        updated = queryset.update(is_featured=True)
        self.message_user(request, f'{updated} products marked as featured.')
    make_featured.short_description = 'Mark selected as Featured'

    def update_ratings(self, request, queryset):
        """Bulk update ratings for selected products"""
        count = 0
        for product in queryset:
            product.update_rating_cache()
            count += 1
        self.message_user(request, f'Updated ratings for {count} products.')
    update_ratings.short_description = 'Update ratings cache'


# ✅ Review Admin - Optimized
@admin.register(Review)
class ReviewAdminOptimized(admin.ModelAdmin):
    list_display = [
        'id',
        'product_link',
        'user_link',
        'rating_stars',
        'is_verified',
        'created_at',
    ]

    list_filter = [
        'rating',
        'is_verified',
        'created_at',
    ]

    search_fields = [
        'product__name',
        'user__username',
        'user__email',
        'comment',
    ]

    list_editable = ['is_verified']
    date_hierarchy = 'created_at'

    # ✅ Queryset optimization
    def get_queryset(self, request):
        queryset = super().get_queryset(request)
        # ✅ select_related for ForeignKeys
        queryset = queryset.select_related('product', 'user')
        return queryset

    def product_link(self, obj):
        url = reverse('admin:shop_product_change', args=[obj.product.id])
        return format_html('<a href="{}">{}</a>', url, obj.product.name)
    product_link.short_description = 'Product'
    product_link.admin_order_field = 'product__name'

    def user_link(self, obj):
        url = reverse('admin:auth_user_change', args=[obj.user.id])
        return format_html('<a href="{}">{}</a>', url, obj.user.username)
    user_link.short_description = 'User'
    user_link.admin_order_field = 'user__username'

    def rating_stars(self, obj):
        return '' * obj.rating
    rating_stars.short_description = 'Rating'
    rating_stars.admin_order_field = 'rating'


# ✅ OrderItem Inline - Optimized
class OrderItemInlineOptimized(admin.TabularInline):
    model = OrderItem
    extra = 0  # ✅ No extra empty forms

    # ✅ Optimize ForeignKey queries
    autocomplete_fields = ['product']  # ✅ Ajax search instead of dropdown

    readonly_fields = ['subtotal_display']

    def subtotal_display(self, obj):
        if obj.id:
            return format_html('<strong>৳{:,.2f}</strong>', obj.subtotal)
        return '-'
    subtotal_display.short_description = 'Subtotal'

    def get_queryset(self, request):
        queryset = super().get_queryset(request)
        # ✅ Prefetch product details
        queryset = queryset.select_related('product')
        return queryset


# ✅ Order Admin - Optimized
@admin.register(Order)
class OrderAdminOptimized(admin.ModelAdmin):
    list_display = [
        'id',
        'user_link',
        'total_amount_display',
        'status_badge',
        'item_count',
        'created_at',
    ]

    list_filter = [
        'status',
        'created_at',
        ('total_amount', admin.NumericRangeFilter),
    ]

    search_fields = [
        'id',
        'user__username',
        'user__email',
    ]

    list_editable = ['status']
    date_hierarchy = 'created_at'

    # ✅ Inline for order items
    inlines = [OrderItemInlineOptimized]

    # ✅ Queryset optimization
    def get_queryset(self, request):
        queryset = super().get_queryset(request)

        # ✅ select_related for user
        queryset = queryset.select_related('user')

        # ✅ Annotate item count
        queryset = queryset.annotate(
            _item_count=Count('items')
        )

        # ✅ prefetch_related for items (for detail view)
        queryset = queryset.prefetch_related(
            'items',
            'items__product'
        )

        return queryset

    def user_link(self, obj):
        url = reverse('admin:auth_user_change', args=[obj.user.id])
        return format_html('<a href="{}">{}</a>', url, obj.user.username)
    user_link.short_description = 'User'
    user_link.admin_order_field = 'user__username'

    def total_amount_display(self, obj):
        return format_html('<strong>৳{:,.2f}</strong>', obj.total_amount)
    total_amount_display.short_description = 'Total'
    total_amount_display.admin_order_field = 'total_amount'

    def status_badge(self, obj):
        colors = {
            'pending': '#ffc107',
            'processing': '#17a2b8',
            'shipped': '#007bff',
            'delivered': '#28a745',
            'cancelled': '#dc3545',
        }
        color = colors.get(obj.status, '#6c757d')
        return format_html(
            '<span style="background:{}; color:white; padding:3px 10px; border-radius:3px; font-weight:bold;">{}</span>',
            color, obj.get_status_display()
        )
    status_badge.short_description = 'Status'
    status_badge.admin_order_field = 'status'

    def item_count(self, obj):
        return obj._item_count
    item_count.short_description = 'Items'
    item_count.admin_order_field = '_item_count'

    # ✅ Custom actions
    actions = ['mark_as_processing', 'mark_as_shipped', 'mark_as_delivered']

    def mark_as_processing(self, request, queryset):
        updated = queryset.update(status='processing')
        self.message_user(request, f'{updated} orders marked as processing.')
    mark_as_processing.short_description = 'Mark as Processing'

    def mark_as_shipped(self, request, queryset):
        updated = queryset.update(status='shipped')
        self.message_user(request, f'{updated} orders marked as shipped.')
    mark_as_shipped.short_description = 'Mark as Shipped'

    def mark_as_delivered(self, request, queryset):
        updated = queryset.update(status='delivered')
        self.message_user(request, f'{updated} orders marked as delivered.')
    mark_as_delivered.short_description = 'Mark as Delivered'


# ✅ OrderItem Admin - Optimized (if needed separately)
@admin.register(OrderItem)
class OrderItemAdminOptimized(admin.ModelAdmin):
    list_display = [
        'id',
        'order_link',
        'product_link',
        'quantity',
        'price_display',
        'subtotal_display',
    ]

    list_filter = ['order__status', 'order__created_at']

    search_fields = [
        'order__id',
        'product__name',
        'order__user__username',
    ]

    # ✅ Ajax autocomplete for ForeignKeys
    autocomplete_fields = ['order', 'product']

    def get_queryset(self, request):
        queryset = super().get_queryset(request)
        # ✅ Optimize all ForeignKey relations
        queryset = queryset.select_related('order', 'order__user', 'product')
        return queryset

    def order_link(self, obj):
        url = reverse('admin:shop_order_change', args=[obj.order.id])
        return format_html('<a href="{}">Order #{}</a>', url, obj.order.id)
    order_link.short_description = 'Order'
    order_link.admin_order_field = 'order__id'

    def product_link(self, obj):
        url = reverse('admin:shop_product_change', args=[obj.product.id])
        return format_html('<a href="{}">{}</a>', url, obj.product.name)
    product_link.short_description = 'Product'
    product_link.admin_order_field = 'product__name'

    def price_display(self, obj):
        return format_html('৳{:,.2f}', obj.price)
    price_display.short_description = 'Price'
    price_display.admin_order_field = 'price'

    def subtotal_display(self, obj):
        return format_html('<strong>৳{:,.2f}</strong>', obj.subtotal)
    subtotal_display.short_description = 'Subtotal'


# ✅ Enable autocomplete for Product (required for autocomplete_fields)
ProductAdminOptimized.search_fields = ['name', 'slug']

📊 Admin Optimization Techniques ব্যাখ্যা

1️⃣ select_related() – ForeignKey Optimization

# ❌ Without select_related (N+1 Problem)
products = Product.objects.all()
for product in products:
    print(product.category.name)  # Extra query per product!
# 100 products = 1 + 100 = 101 queries

# ✅ With select_related
products = Product.objects.select_related('category')
for product in products:
    print(product.category.name)  # No extra query!
# 100 products = 1 query (JOIN করে)

কখন ব্যবহার করবেন:

  • ForeignKey (Many-to-One)
  • OneToOneField

2️⃣ prefetch_related() – Reverse ForeignKey & ManyToMany

# ❌ Without prefetch_related
orders = Order.objects.all()
for order in orders:
    print(order.items.count())  # Extra query per order!
# 100 orders = 1 + 100 = 101 queries

# ✅ With prefetch_related
orders = Order.objects.prefetch_related('items')
for order in orders:
    print(order.items.count())  # No extra query!
# 100 orders = 2 queries (1 main + 1 prefetch)

কখন ব্যবহার করবেন:

  • Reverse ForeignKey (One-to-Many)
  • ManyToManyField

3️⃣ annotate() – Aggregate Data

# ❌ Without annotate
categories = Category.objects.all()
for cat in categories:
    count = cat.products.count()  # Extra query!

# ✅ With annotate
categories = Category.objects.annotate(
    product_count=Count('products')
)
for cat in categories:
    count = cat.product_count  # No extra query!

4️⃣ only() vs defer()

# ✅ only(): শুধু নির্দিষ্ট fields load করুন
products = Product.objects.only('id', 'name', 'price')
# SELECT id, name, price FROM product

# ✅ defer(): নির্দিষ্ট fields বাদ দিন
products = Product.objects.defer('description')
# SELECT id, name, price, ... (description ছাড়া সব)

5️⃣ autocomplete_fields – Ajax Search

# ❌ Without autocomplete (Dropdown with all products)
# 10,000 products = page load slow!

# ✅ With autocomplete (Ajax search)
autocomplete_fields = ['product']
# শুধু search করলে load হয়, fast!

🧪 Admin Performance Test

test_admin_performance.py তৈরি করুন:

import os
import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'optimization_project.settings')
django.setup()

from django.test.utils import override_settings
from django.db import connection, reset_queries
from django.contrib.admin.sites import site
from shop.models import Product
from shop.admin import ProductAdminOptimized
from django.test import RequestFactory

# Enable query logging
@override_settings(DEBUG=True)
def test_admin_queries():
    # Create fake request
    factory = RequestFactory()
    request = factory.get('/admin/shop/product/')
    request.user = None

    # Test Normal Admin (without optimization)
    print("=" * 60)
    print("🔴 Testing NORMAL Admin (No Optimization)")
    print("=" * 60)

    reset_queries()

    # Simulate normal queryset
    products = Product.objects.all()[:100]
    for product in products:
        _ = product.category.name  # Trigger query
        _ = product.brand.name     # Trigger query

    print(f"Total Queries: {len(connection.queries)}")
    print(f"Time: {sum(float(q['time']) for q in connection.queries):.3f}s")

    # Test Optimized Admin
    print("\n" + "=" * 60)
    print("✅ Testing OPTIMIZED Admin")
    print("=" * 60)

    reset_queries()

    # Simulate optimized queryset
    admin_instance = ProductAdminOptimized(Product, site)
    products = admin_instance.get_queryset(request)[:100]
    for product in products:
        _ = product.category.name  # No extra query!
        _ = product.brand.name     # No extra query!

    print(f"Total Queries: {len(connection.queries)}")
    print(f"Time: {sum(float(q['time']) for q in connection.queries):.3f}s")

    print("\n" + "=" * 60)
    print("📊 RESULT")
    print("=" * 60)

if __name__ == '__main__':
    test_admin_queries()

চালান:

python test_admin_performance.py

Expected Output:

============================================================
🔴 Testing NORMAL Admin (No Optimization)
============================================================
Total Queries: 201
Time: 0.850s

============================================================
 Testing OPTIMIZED Admin
============================================================
Total Queries: 1
Time: 0.008s

============================================================
📊 RESULT
============================================================
Optimized admin is 106x faster! 🚀

📝 Admin Optimization Checklist

✅ করণীয়:

  1. select_related() – সব ForeignKey relations এ
  2. prefetch_related() – Reverse relations এ
  3. annotate() – Count/Avg/Sum calculations এ
  4. list_select_related – Admin list view এ
  5. autocomplete_fields – Large ForeignKey dropdowns এ
  6. search_fields – Indexed fields এ search
  7. list_filter – Indexed fields এ filter
  8. readonly_fields – Calculated fields এ
  9. list_editable – Quick edit এর জন্য
  10. actions – Bulk operations এর জন্য

❌ এড়িয়ে চলুন:

  1. List display তে complex calculations
  2. Non-indexed fields এ search/filter
  3. Too many list_display fields
  4. Large text fields list display তে
  5. Nested loops in display methods

How can we help?