Personalizacja Panelu Administracyjnego Django w 2026: Akcje, Filtry i Pytania Rekrutacyjne

Kompletny przewodnik po personalizacji panelu administracyjnego Django 6 z niestandardowymi akcjami, filtrami, fieldsetami i inline. Zawiera pytania rekrutacyjne.

Personalizacja Panelu Administracyjnego Django w 2026: Akcje, Filtry i Pytania Rekrutacyjne

Personalizacja panelu administracyjnego Django przekształca wbudowany interfejs z prostego narzędzia do zarządzania danymi w profesjonalne zaplecze dla aplikacji produkcyjnych. Django 6 wprowadza ulepszone funkcje administracyjne, w tym liczniki facetów dla filtrów, konfigurowalne formularze zmiany hasła oraz ikony Font Awesome, dzięki czemu dobrze skonfigurowany panel administracyjny staje się przyjemny w użyciu.

Kluczowa Informacja

ModelAdmin to centralna klasa dla całej personalizacji panelu administracyjnego. Opanowanie list_display, list_filter, search_fields, fieldsets oraz niestandardowych akcji pozwala obsłużyć 90% potrzeb konfiguracyjnych panelu.

Podstawy ModelAdmin: list_display i search_fields

list_display kontroluje, które kolumny pojawiają się na stronie listy zmian. Oprócz prostych nazw pól, akceptuje funkcje wywoływalne opatrzone dekoratorem @admin.display() dla wartości obliczanych. search_fields włącza pole wyszukiwania, które domyślnie filtruje rekordy przy użyciu icontains.

python
# admin.py
from django.contrib import admin
from django.utils.html import format_html
from .models import Article


class ArticleAdmin(admin.ModelAdmin):
    list_display = ["title", "author_name", "status", "publish_date", "is_recent"]
    search_fields = ["title", "author__email", "content"]
    list_per_page = 50
    list_select_related = ["author"]  # Avoid N+1 queries

    @admin.display(description="Author", ordering="author__last_name")
    def author_name(self, obj):
        return f"{obj.author.first_name} {obj.author.last_name}"

    @admin.display(description="Recent?", boolean=True)
    def is_recent(self, obj):
        from django.utils import timezone
        return obj.publish_date >= timezone.now() - timezone.timedelta(days=7)


admin.site.register(Article, ArticleAdmin)

Dekorator @admin.display() zastępuje starszy wzorzec ustawiania short_description jako atrybutu funkcji. Parametr boolean=True renderuje ikonę znacznika lub X zamiast tekstu True/False. Użycie list_select_related zapobiega zapytaniom N+1 podczas dostępu do pól kluczy obcych w list_display.

Niestandardowe list_filter z SimpleListFilter

list_filter aktywuje filtry na pasku bocznym strony listy zmian. Nazwy pól działają dla podstawowego filtrowania, ale SimpleListFilter obsługuje złożoną logikę filtrowania obejmującą wiele pól lub wymagającą niestandardowych zapytań.

python
# admin.py
from datetime import date, timedelta
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from .models import Article


class PublishDateFilter(admin.SimpleListFilter):
    title = _("publish period")
    parameter_name = "published"  # URL parameter: ?published=this_week

    def lookups(self, request, model_admin):
        # Returns (value, label) tuples for the filter sidebar
        return [
            ("this_week", _("This week")),
            ("this_month", _("This month")),
            ("this_year", _("This year")),
            ("unpublished", _("Not published")),
        ]

    def queryset(self, request, queryset):
        # Filter the queryset based on self.value()
        today = date.today()
        if self.value() == "this_week":
            return queryset.filter(publish_date__gte=today - timedelta(days=7))
        if self.value() == "this_month":
            return queryset.filter(
                publish_date__year=today.year, publish_date__month=today.month
            )
        if self.value() == "this_year":
            return queryset.filter(publish_date__year=today.year)
        if self.value() == "unpublished":
            return queryset.filter(publish_date__isnull=True)
        return queryset


class ArticleAdmin(admin.ModelAdmin):
    list_filter = [
        "status",
        PublishDateFilter,
        ("author", admin.RelatedOnlyFieldListFilter),  # Show only authors with articles
    ]

RelatedOnlyFieldListFilter ogranicza listę rozwijaną autorów tylko do tych, którzy faktycznie mają artykuły, zamiast wyświetlać każdego użytkownika w bazie danych. To poprawia użyteczność, gdy powiązany model zawiera wiele rekordów.

Facety w Django 6: Liczniki Filtrów na Pasku Bocznym

Django 6 wprowadziło liczniki facetów, które wyświetlają liczbę pasujących obiektów obok każdej opcji filtra. Atrybut show_facets kontroluje to zachowanie.

python
# admin.py
from django.contrib import admin
from django.contrib.admin import ShowFacets
from .models import Article


class ArticleAdmin(admin.ModelAdmin):
    list_display = ["title", "status", "category"]
    list_filter = ["status", "category", "author"]
    show_facets = ShowFacets.ALWAYS  # Always display counts

    # Alternative values:
    # ShowFacets.ALLOW - Show counts when ?_facets=1 in URL
    # ShowFacets.NEVER - Never show counts (for large datasets)

Liczniki facetów wykonują dodatkowe zapytania COUNT dla każdej opcji filtra. Na tabelach z milionami wierszy należy ustawić show_facets = ShowFacets.NEVER, aby uniknąć degradacji wydajności. Liczniki aktualizują się dynamicznie w miarę stosowania filtrów, pokazując ile rekordów pasuje do bieżącej kombinacji filtrów.

Niestandardowe Akcje Administracyjne dla Operacji Zbiorczych

Akcje administracyjne wykonują operacje zbiorcze na wybranych obiektach. Dekorator @admin.action ustawia uprawnienia oraz opis wyświetlany w menu rozwijanym.

python
# admin.py
from django.contrib import admin
from django.contrib import messages
from django.utils.translation import ngettext
from .models import Article


@admin.action(description="Publish selected articles", permissions=["change"])
def publish_articles(modeladmin, request, queryset):
    # Exclude already published articles
    unpublished = queryset.exclude(status="published")
    updated = unpublished.update(status="published")

    modeladmin.message_user(
        request,
        ngettext(
            "%d article was published.",
            "%d articles were published.",
            updated,
        )
        % updated,
        messages.SUCCESS,
    )


@admin.action(description="Export selected as JSON")
def export_as_json(modeladmin, request, queryset):
    from django.http import HttpResponse
    from django.core import serializers

    response = HttpResponse(content_type="application/json")
    response["Content-Disposition"] = 'attachment; filename="articles.json"'
    serializers.serialize("json", queryset, stream=response)
    return response  # Returning HttpResponse triggers file download


class ArticleAdmin(admin.ModelAdmin):
    actions = [publish_articles, export_as_json]

    def get_actions(self, request):
        # Conditionally remove actions based on user permissions
        actions = super().get_actions(request)
        if not request.user.has_perm("articles.delete_article"):
            del actions["delete_selected"]
        return actions

Zwrócenie HttpResponse z akcji uruchamia pobieranie pliku lub przekierowuje na stronę potwierdzenia. Parametr permissions ogranicza akcję do użytkowników z określonymi uprawnieniami modelu. Nadpisanie get_actions() pozwala dynamicznie pokazywać lub ukrywać akcje na podstawie bieżącego żądania.

Gotowy na rozmowy o Django?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Fieldsets i Modele Inline dla Zorganizowanych Formularzy

fieldsets organizuje formularze dodawania i edycji w zwijane sekcje. TabularInline i StackedInline osadzają powiązane modele bezpośrednio w formularzu nadrzędnym, eliminując nawigację między stronami.

python
# admin.py
from django.contrib import admin
from .models import Article, ArticleImage, ArticleComment


class ArticleImageInline(admin.TabularInline):
    model = ArticleImage
    extra = 1  # Number of empty forms to display
    max_num = 5  # Maximum images per article
    fields = ["image", "caption", "order"]


class ArticleCommentInline(admin.StackedInline):
    model = ArticleComment
    extra = 0
    readonly_fields = ["created_at", "author"]
    can_delete = True


class ArticleAdmin(admin.ModelAdmin):
    fieldsets = [
        (
            None,
            {
                "fields": ["title", "slug", "author"],
            },
        ),
        (
            "Content",
            {
                "fields": ["content", "excerpt"],
                "classes": ["wide"],  # Full-width fields
            },
        ),
        (
            "Publishing",
            {
                "fields": ["status", "publish_date", "category", "tags"],
                "classes": ["collapse"],  # Collapsed by default
            },
        ),
        (
            "SEO",
            {
                "fields": ["meta_title", "meta_description"],
                "classes": ["collapse"],
                "description": "Search engine optimization fields",
            },
        ),
    ]
    inlines = [ArticleImageInline, ArticleCommentInline]
    prepopulated_fields = {"slug": ["title"]}  # Auto-generate slug from title
    autocomplete_fields = ["author", "category"]  # AJAX-powered select
    filter_horizontal = ["tags"]  # Widget for M2M selection


admin.site.register(Article, ArticleAdmin)

TabularInline wyświetla powiązane obiekty w kompaktowym formacie tabeli, odpowiednim dla prostych modeli z kilkoma polami. StackedInline pokazuje każdy powiązany obiekt jako oddzielny blok formularza, lepszy dla modeli z wieloma polami. Opcja autocomplete_fields zastępuje domyślną listę rozwijaną widżetem wyszukiwania AJAX, niezbędnym gdy powiązany model zawiera tysiące rekordów.

Personalizacja Witryny Administracyjnej

Podklasowanie AdminSite umożliwia tworzenie wielu interfejsów administracyjnych z różnym brandingiem, uprawnieniami lub rejestracjami modeli. Django 6 dodało możliwość personalizacji password_change_form dla bardziej rygorystycznych polityk haseł.

python
# admin.py
from django.contrib import admin
from django.contrib.admin import AdminSite
from .models import Article, Category


class ContentAdminSite(AdminSite):
    site_header = "Content Management"
    site_title = "CMS"
    index_title = "Dashboard"

    def has_permission(self, request):
        # Restrict to staff in the 'editors' group
        return (
            request.user.is_active
            and request.user.is_staff
            and request.user.groups.filter(name="editors").exists()
        )


content_admin = ContentAdminSite(name="content_admin")
content_admin.register(Article, ArticleAdmin)
content_admin.register(Category)

# In urls.py:
# path('content-admin/', content_admin.urls),

Każda instancja AdminSite ma własną przestrzeń nazw URL, stronę logowania oraz zestaw zarejestrowanych modeli. Ten wzorzec pasuje do aplikacji z odrębnymi rolami użytkowników, gdzie redaktorzy nie powinni widzieć zarządzania użytkownikami ani ustawień systemowych.

Pytania Rekrutacyjne: Personalizacja Panelu Admin Django

Te pytania często pojawiają się na technicznych rozmowach kwalifikacyjnych Django. Zrozumienie mechanizmów działania, a nie tylko składni, odróżnia kandydatów średniego szczebla od starszych inżynierów.

P: Jak dodać kolumnę obliczaną do list_display, która nie może być sortowana?

Dekorator @admin.display() przyjmuje parametr ordering, który określa które pole bazy danych użyć do sortowania. Bez niego nagłówek kolumny nie jest klikalny. Wartości obliczane agregujące wiele rekordów lub wywołujące usługi zewnętrzne nie mogą być sortowane, ponieważ nie ma bazowej kolumny bazy danych do uporządkowania.

P: Jaki jest wpływ liczników facetów na wydajność i kiedy należy je wyłączyć?

Facety wykonują jedno zapytanie COUNT na opcję filtra, pomnożone przez liczbę aktywnych filtrów. Na tabeli z 10 milionami wierszy i 5 opcjami filtra na 3 polach, to 15 dodatkowych zapytań COUNT na każde załadowanie strony. Należy ustawić show_facets = ShowFacets.NEVER gdy jakikolwiek filtr dotyczy tabeli powyżej 100 000 wierszy bez odpowiednich indeksów.

P: Czym różnią się akcje administracyjne od niestandardowych widoków administratora?

Akcje operują na queryset wybranych obiektów i integrują się z polami wyboru listy zmian. Niestandardowe widoki administratora to samodzielne strony dostępne przez niestandardowe adresy URL, odpowiednie dla raportów, importów lub dashboardów, które nie operują na wybranych rekordach. Akcje nie powinny wykonywać długotrwałych operacji, ponieważ blokują żądanie.

P: Dlaczego używać RelatedOnlyFieldListFilter zamiast domyślnego filtra?

Domyślny RelatedFieldListFilter odpytuje wszystkie obiekty w powiązanej tabeli. Gdy klucz obcy wskazuje na model User ze 100 000 rekordami, rozwijana lista filtra ładuje wszystkie 100 000 opcji, zamrażając przeglądarkę. RelatedOnlyFieldListFilter odpytuje tylko wartości istniejące w rekordach bieżącego modelu, zazwyczaj znacznie mniejszy zbiór.

Typowe Błędy do Unikania w Produkcji

Kilka wzorców działających w środowisku deweloperskim powoduje problemy na skalę.

python
# admin.py
from django.contrib import admin
from .models import Order


class OrderAdmin(admin.ModelAdmin):
    # Mistake: Accessing related fields without list_select_related
    # This causes N+1 queries, one per row in the list view
    list_display = ["id", "customer_email", "total"]

    # Fix: Prefetch related objects
    list_select_related = ["customer"]

    def customer_email(self, obj):
        return obj.customer.email

    # Mistake: search_fields on unindexed large text columns
    # search_fields = ["notes"]  # Slow on large tables

    # Fix: Limit search to indexed fields
    search_fields = ["id", "customer__email"]

    # Mistake: Expensive computation in list_display
    # def order_total(self, obj):
    #     return sum(item.price * item.qty for item in obj.items.all())  # N+1

    # Fix: Use annotation in get_queryset
    def get_queryset(self, request):
        from django.db.models import Sum, F

        qs = super().get_queryset(request)
        return qs.annotate(computed_total=Sum(F("items__price") * F("items__qty")))

    @admin.display(description="Total", ordering="computed_total")
    def total(self, obj):
        return obj.computed_total

Nadpisanie get_queryset() w celu dodania adnotacji utrzymuje obliczane wartości w jednym zapytaniu, zamiast odpytywać bazę danych raz na wiersz. Profilowanie panelu administracyjnego za pomocą Django Debug Toolbar pozwala wychwycić zapytania N+1 zanim trafią do produkcji.

Motywy Zewnętrzne dla Panelu Admin: Django Unfold

Django Unfold to najaktywniej rozwijany motyw administracyjny dla Django 6, zbudowany z Tailwind CSS. Rozszerza standardowy panel administracyjny zamiast go zastępować, więc istniejące konfiguracje ModelAdmin nadal działają.

python
# settings.py
INSTALLED_APPS = [
    "unfold",  # Must come before django.contrib.admin
    "unfold.contrib.filters",  # Advanced filter widgets
    "unfold.contrib.forms",  # Enhanced form widgets
    "django.contrib.admin",
    # ...
]

Unfold dodaje tryb ciemny, responsywne układy, nawigację boczną oraz zaawansowane typy filtrów jak RangeNumericFilter i RangeDateFilter. Integracja z HTMX i Alpine.js zapewnia interaktywność bez obciążenia pełnego frameworka SPA. Dla zespołów potrzebujących dopracowanego panelu administracyjnego bez budowania niestandardowego dashboardu, Unfold oszczędza tygodnie pracy frontendowej.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Co Zapamiętać o Personalizacji Panelu Admin Django

  • list_display z @admin.display() kontroluje kolumny; dodanie ordering umożliwia sortowanie wartości obliczanych
  • SimpleListFilter obsługuje niestandardową logikę filtrowania obejmującą wiele pól lub wymagającą zakresów dat
  • Facety Django 6 pokazują liczniki filtrów; wyłączenie za pomocą ShowFacets.NEVER na dużych tabelach pozwala uniknąć narzutu zapytań COUNT
  • Akcje administracyjne z @admin.action(permissions=[...]) obsługują operacje zbiorcze; zwrócenie HttpResponse umożliwia eksport plików
  • Użycie list_select_related i adnotowanych queryset w get_queryset() zapobiega zapytaniom N+1 w widokach list
  • RelatedOnlyFieldListFilter zapobiega zamrażaniu przeglądarki podczas filtrowania po kluczu obcym do dużych tabel
  • Praktykowanie tych wzorców z pytaniami rekrutacyjnymi Django ukierunkowanymi na konfigurację panelu administracyjnego
Wyzwanie dnia

Znajdziesz błąd w Django?

Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Anthony Fillion-Maillet

Autor:

Anthony Fillion-Maillet

Założyciel SharpSkill

Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.

Zaktualizowano 13 września 2026

Udostępnij

Powiązane artykuły