Claude Skill

unfold-admin

Django Unfold admin theme - build, configure, and enhance modern Django admin interfaces with Unfold. Use when working with: (1) Django admin UI customisation or theming, (2) Unfold ModelAdmin, inlines, actions, filters, widgets, or decorators, (3) Admin dashboard components and

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_unfold-admin-3dfaf0b.zip · 28 KB
Part of 0xdarkmatter/claude-mods — 94 skills

Install

skills CLI npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/unfold-admin
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git git clone https://github.com/0xDarkMatter/claude-mods.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Django Unfold Admin

Modern Django admin theme with Tailwind CSS, HTMX, and Alpine.js. Replaces Django's default admin with a polished, feature-rich interface.

Quick Start

Installation

# settings.py - unfold MUST be before django.contrib.admin
INSTALLED_APPS = [
    "unfold",
    "unfold.contrib.filters",          # advanced filters
    "unfold.contrib.forms",            # array/wysiwyg widgets
    "unfold.contrib.inlines",          # nonrelated inlines
    "unfold.contrib.import_export",    # styled import/export
    "unfold.contrib.guardian",         # django-guardian integration
    "unfold.contrib.simple_history",   # django-simple-history integration
    "unfold.contrib.constance",        # django-constance integration
    "unfold.contrib.location_field",   # django-location-field integration
    # ...
    "django.contrib.admin",
]

Minimal Admin

from unfold.admin import ModelAdmin

@admin.register(MyModel)
class MyModelAdmin(ModelAdmin):
    pass  # inherits Unfold styling

Site Configuration

Replace the default AdminSite or configure via UNFOLD dict in settings. See references/configuration.md for the complete settings reference.

UNFOLD = {
    "SITE_TITLE": "My Admin",
    "SITE_HEADER": "My Admin",
    "SITE_SYMBOL": "dashboard",  # Material Symbols icon name
    "SIDEBAR": {
        "show_search": True,
        "navigation": [
            {
                "title": _("Navigation"),
                "items": [
                    {
                        "title": _("Dashboard"),
                        "icon": "dashboard",
                        "link": reverse_lazy("admin:index"),
                    },
                ],
            },
        ],
    },
}

Core Workflow

When building Unfold admin interfaces, follow this sequence:

  1. Configure site - UNFOLD settings dict (branding, sidebar, theme)
  2. Register models - Extend unfold.admin.ModelAdmin
  3. Enhance display - @display decorator for list columns
  4. Add actions - @action decorator for row/list/detail/submit actions
  5. Configure filters - Replace default filters with Unfold filter classes
  6. Override widgets - Apply Unfold widgets via formfield_overrides
  7. Set up inlines - Use Unfold's inline classes with tabs, pagination, sorting
  8. Build dashboard - @register_component + BaseComponent for KPI cards

ModelAdmin Attributes

Unfold extends Django's ModelAdmin with these additional attributes:

Attribute Type Purpose
list_fullwidth bool Full-width changelist (no sidebar)
list_filter_submit bool Add submit button to filters
list_filter_sheet bool Filters in sliding sheet panel
compressed_fields bool Compact field spacing in forms
warn_unsaved_form bool Warn before leaving unsaved form
ordering_field str Field name for drag-to-reorder
hide_ordering_field bool Hide the ordering field column
list_horizontal_scrollbar_top bool Scrollbar at top of list
list_disable_select_all bool Disable "select all" checkbox
change_form_show_cancel_button bool Show cancel button on form
actions_list list Global changelist actions
actions_row list Per-row actions in changelist
actions_detail list Actions on change form
actions_submit_line list Actions in form submit area
actions_list_hide_default bool Hide default list actions
actions_detail_hide_default bool Hide default detail actions
conditional_fields dict JS expressions for field visibility
change_form_datasets list BaseDataset subclasses for change form
list_sections list TableSection/TemplateSection for list
list_sections_classes str CSS grid classes for sections
readonly_preprocess_fields dict Transform readonly field content
add_fieldsets list Separate fieldsets for add form (like UserAdmin)

Template Injection Points

Insert custom HTML before/after changelist or change form:

class MyAdmin(ModelAdmin):
    # Changelist
    list_before_template = "myapp/list_before.html"
    list_after_template = "myapp/list_after.html"
    # Change form (inside <form> tag)
    change_form_before_template = "myapp/form_before.html"
    change_form_after_template = "myapp/form_after.html"
    # Change form (outside <form> tag)
    change_form_outer_before_template = "myapp/outer_before.html"
    change_form_outer_after_template = "myapp/outer_after.html"

Conditional Fields

Show/hide fields based on other field values (Alpine.js expressions):

class MyAdmin(ModelAdmin):
    conditional_fields = {
        "premium_features": "plan == 'PRO'",
        "discount_amount": "has_discount == true",
    }

Actions System

Four action types, each with different signatures. See references/actions-filters.md for complete reference.

from unfold.decorators import action
from unfold.enums import ActionVariant

# List action (no object context)
@action(description=_("Rebuild Index"), icon="sync", variant=ActionVariant.PRIMARY)
def rebuild_index(self, request):
    # process...
    return redirect(request.headers["referer"])

# Row action (receives object_id)
@action(description=_("Approve"), url_path="approve")
def approve_row(self, request, object_id):
    obj = self.model.objects.get(pk=object_id)
    return redirect(request.headers["referer"])

# Detail action (receives object_id, shown on change form)
@action(description=_("Send Email"), permissions=["send_email"])
def send_email(self, request, object_id):
    return redirect(reverse_lazy("admin:myapp_mymodel_change", args=[object_id]))

# Submit line action (receives obj instance, runs on save)
@action(description=_("Save & Publish"))
def save_and_publish(self, request, obj):
    obj.published = True

Action Groups (Dropdown Menus)

actions_list = [
    "primary_action",
    {
        "title": _("More"),
        "variant": ActionVariant.PRIMARY,
        "items": ["secondary_action", "tertiary_action"],
    },
]

Permissions

@action(permissions=["can_export", "auth.view_user"])
def export_data(self, request):
    pass

def has_can_export_permission(self, request):
    return request.user.is_superuser

Display Decorator

Enhance list_display columns. See references/actions-filters.md.

from unfold.decorators import display

# Colored status labels
@display(description=_("Status"), ordering="status", label={
    "active": "success",    # green
    "pending": "info",      # blue
    "warning": "warning",   # orange
    "inactive": "danger",   # red
})
def show_status(self, obj):
    return obj.status

# Rich header with avatar
@display(description=_("User"), header=True)
def show_header(self, obj):
    return [
        obj.full_name,           # primary text
        obj.email,               # secondary text
        obj.initials,            # badge text
        {"path": obj.avatar.url, "width": 24, "height": 24, "borderless": True},
    ]

# Interactive dropdown
@display(description=_("Teams"), dropdown=True)
def show_teams(self, obj):
    return {
        "title": f"{obj.teams.count()} teams",
        "items": [{"title": t.name, "link": t.get_admin_url()} for t in obj.teams.all()],
        "striped": True,
        "max_height": 200,
    }

# Boolean checkmark
@display(description=_("Active"), boolean=True)
def is_active(self, obj):
    return obj.is_active

Filters

Unfold provides advanced filter classes. See references/actions-filters.md.

from unfold.contrib.filters.admin import (
    TextFilter, RangeNumericFilter, RangeDateFilter, RangeDateTimeFilter,
    SingleNumericFilter, SliderNumericFilter, RelatedDropdownFilter,
    RelatedCheckboxFilter, ChoicesCheckboxFilter, AllValuesCheckboxFilter,
    BooleanRadioFilter, CheckboxFilter, AutocompleteSelectMultipleFilter,
)

class MyAdmin(ModelAdmin):
    list_filter_submit = True  # required for input-based filters
    list_filter = [
        ("salary", RangeNumericFilter),
        ("status", ChoicesCheckboxFilter),
        ("created_at", RangeDateFilter),
        ("category", RelatedDropdownFilter),
        ("is_active", BooleanRadioFilter),
    ]

Custom Text Filter

class NameFilter(TextFilter):
    title = _("Name")
    parameter_name = "name"

    def queryset(self, request, queryset):
        if self.value() in EMPTY_VALUES:
            return queryset
        return queryset.filter(name__icontains=self.value())

Widgets

Override form widgets for Unfold styling. See references/widgets-inlines.md.

from unfold.widgets import (
    UnfoldAdminTextInputWidget, UnfoldAdminSelectWidget, UnfoldAdminSelect2Widget,
    UnfoldBooleanSwitchWidget, UnfoldAdminColorInputWidget,
    UnfoldAdminSplitDateTimeWidget, UnfoldAdminImageFieldWidget,
)
from unfold.contrib.forms.widgets import WysiwygWidget, ArrayWidget

class MyAdmin(ModelAdmin):
    formfield_overrides = {
        models.TextField: {"widget": WysiwygWidget},
        models.ImageField: {"widget": UnfoldAdminImageFieldWidget},
    }

Text Input with Icons

widget = UnfoldAdminTextInputWidget(attrs={
    "prefix_icon": "search",
    "suffix_icon": "euro",
})

Inlines

Unfold inlines support tabs, pagination, sorting, and nonrelated models. See references/widgets-inlines.md.

from unfold.admin import TabularInline, StackedInline
from unfold.contrib.inlines.admin import NonrelatedStackedInline

class OrderItemInline(TabularInline):
    model = OrderItem
    tab = True          # show as tab
    per_page = 10       # paginated
    ordering_field = "weight"  # drag-to-reorder
    hide_title = True
    collapsible = True

Fieldset Tabs

Group fieldsets into tabs using "classes": ["tab"]:

fieldsets = [
    (None, {"fields": ["name", "email"]}),  # always visible
    (_("Profile"), {"classes": ["tab"], "fields": ["bio", "avatar"]}),
    (_("Settings"), {"classes": ["tab"], "fields": ["theme", "notifications"]}),
]

Dashboard Components

Build KPI cards and custom dashboard widgets. See references/dashboard.md.

from unfold.components import BaseComponent, register_component
from django.template.loader import render_to_string

@register_component
class ActiveUsersComponent(BaseComponent):
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["children"] = render_to_string("myapp/kpi_card.html", {
            "total": User.objects.filter(is_active=True).count(),
            "label": "Active Users",
        })
        return context

Configure in settings:

UNFOLD = {
    "DASHBOARD_CALLBACK": "myapp.views.dashboard_callback",
}

Sections (Changelist Panels)

Embed related data panels in changelist views:

from unfold.sections import TableSection, TemplateSection

class RecentOrdersSection(TableSection):
    related_name = "order_set"
    fields = ["id", "total", "status"]
    height = 380

class ChartSection(TemplateSection):
    template_name = "myapp/chart.html"

class MyAdmin(ModelAdmin):
    list_sections = [RecentOrdersSection, ChartSection]
    list_sections_classes = "lg:grid-cols-2"

Datasets (Change Form Panels)

Embed model listings within change forms:

from unfold.datasets import BaseDataset

class RelatedItemsDatasetAdmin(ModelAdmin):
    list_display = ["name", "status"]
    search_fields = ["name"]

class RelatedItemsDataset(BaseDataset):
    model = RelatedItem
    model_admin = RelatedItemsDatasetAdmin
    tab = True  # show as tab

class MyAdmin(ModelAdmin):
    change_form_datasets = [RelatedItemsDataset]

Paginator

Use infinite scroll pagination:

from unfold.paginator import InfinitePaginator

class MyAdmin(ModelAdmin):
    paginator = InfinitePaginator
    show_full_result_count = False
    list_per_page = 20

Third-Party Integrations

Unfold provides styled wrappers for common Django packages. See references/resources.md for complete setup guides.

Package Unfold Module Setup
django-import-export unfold.contrib.import_export Use ImportForm, ExportForm, SelectableFieldsExportForm
django-guardian unfold.contrib.guardian Styled guardian integration
django-simple-history unfold.contrib.simple_history Styled history integration
django-constance unfold.contrib.constance Styled constance config
django-location-field unfold.contrib.location_field Location widget
django-modeltranslation Compatible Mix TabbedTranslationAdmin with ModelAdmin
django-celery-beat Compatible (rewire) Unregister 5 models, re-register with Unfold
django-money unfold.widgets UnfoldAdminMoneyWidget
djangoql Compatible Mix DjangoQLSearchMixin with ModelAdmin
django-crispy-forms Compatible Unfold template pack available
# Multiple inheritance - Unfold ModelAdmin always last
@admin.register(MyModel)
class MyAdmin(DjangoQLSearchMixin, SimpleHistoryAdmin, GuardedModelAdmin, ModelAdmin):
    pass

Built-In Template Components

Unfold ships reusable template components for dashboards and custom pages:

Component Path Key Variables
Card unfold/components/card.html title, footer, label, icon
Bar Chart unfold/components/chart/bar.html data (JSON), height, width
Line Chart unfold/components/chart/line.html data (JSON), height, width
Progress unfold/components/progress.html value, title, description
Table unfold/components/table.html table, card_included, striped
Button unfold/components/button.html name, href, submit
Tracker unfold/components/tracker.html data
Cohort unfold/components/cohort.html data
{% load unfold %}
{% component "MyKPIComponent" %}{% endcomponent %}

User Admin Forms

Unfold provides styled versions of Django's auth admin forms:

from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm

@admin.register(User)
class UserAdmin(BaseUserAdmin, ModelAdmin):
    form = UserChangeForm
    add_form = UserCreationForm
    change_password_form = AdminPasswordChangeForm

Reference Files

Detailed documentation split by topic:

Read the relevant reference file when you need detailed configuration options, the full list of available classes, complete code examples, or integration setup guides for a specific feature area.

Key External References

Resource URL
Docs https://unfoldadmin.com/docs/
GitHub https://github.com/unfoldadmin/django-unfold
Demo App (Formula) https://github.com/unfoldadmin/formula
Live Demo https://demo.unfoldadmin.com
Material Symbols (Icons) https://fonts.google.com/icons

When uncertain about an implementation pattern, consult formula/admin.py and formula/settings.py in the Formula demo repo - it covers virtually every Unfold feature.

Files (claude-mods)
  • references
    • actions-filters.md 13.6 KB
      # Actions, Display Decorators, and Filters Reference
      
      ## Table of Contents
      
      - [Action System](#action-system)
      - [Display Decorator](#display-decorator)
      - [Filter Classes](#filter-classes)
      - [Custom Filters](#custom-filters)
      
      ## Action System
      
      ### Imports
      
      ```python
      from unfold.decorators import action
      from unfold.enums import ActionVariant
      ```
      
      ### @action Decorator Parameters
      
      | Parameter | Type | Description |
      |-----------|------|-------------|
      | `description` | str | Button label text |
      | `icon` | str | Material Symbols icon name |
      | `variant` | ActionVariant | Button color/style |
      | `permissions` | list[str] | Required permission names |
      | `url_path` | str | Custom URL path segment |
      | `attrs` | dict | Extra HTML attributes |
      
      ### ActionVariant Enum
      
      ```python
      from unfold.enums import ActionVariant
      
      ActionVariant.DEFAULT   # neutral
      ActionVariant.PRIMARY   # primary color
      ActionVariant.SUCCESS   # green
      ActionVariant.INFO      # blue
      ActionVariant.WARNING   # orange
      ActionVariant.DANGER    # red
      ```
      
      ### Four Action Types
      
      Each type has a different method signature and registration attribute:
      
      #### 1. List Actions (Global)
      
      Appear at top of changelist. No object context.
      
      ```python
      class MyAdmin(ModelAdmin):
          actions_list = ["rebuild_index"]
      
          @action(description=_("Rebuild Index"), icon="sync", variant=ActionVariant.PRIMARY)
          def rebuild_index(self, request):
              # perform global operation
              messages.success(request, _("Index rebuilt."))
              return redirect(request.headers["referer"])
      ```
      
      #### 2. Row Actions
      
      Per-row buttons in changelist. Receive `object_id`.
      
      ```python
      class MyAdmin(ModelAdmin):
          actions_row = ["approve_item"]
      
          @action(description=_("Approve"), url_path="approve-item")
          def approve_item(self, request, object_id):
              obj = self.model.objects.get(pk=object_id)
              obj.approved = True
              obj.save()
              messages.success(request, f"Approved {obj}")
              return redirect(
                  request.headers.get("referer")
                  or reverse_lazy("admin:myapp_mymodel_changelist")
              )
      ```
      
      #### 3. Detail Actions
      
      Buttons on change form toolbar. Receive `object_id`.
      
      ```python
      class MyAdmin(ModelAdmin):
          actions_detail = ["send_notification"]
      
          @action(
              description=_("Send Notification"),
              url_path="send-notification",
              permissions=["send_notification"],
          )
          def send_notification(self, request, object_id):
              obj = get_object_or_404(self.model, pk=object_id)
              # can render a custom form page
              return render(request, "myapp/notification_form.html", {
                  "object": obj,
                  **self.admin_site.each_context(request),
              })
      
          def has_send_notification_permission(self, request, object_id=None):
              return request.user.has_perm("myapp.send_notification")
      ```
      
      #### 4. Submit Line Actions
      
      Execute during form save. Receive the model instance `obj`.
      
      ```python
      class MyAdmin(ModelAdmin):
          actions_submit_line = ["save_and_publish"]
      
          @action(description=_("Save & Publish"), permissions=["publish"])
          def save_and_publish(self, request, obj):
              obj.published = True
              messages.success(request, f"Published {obj}")
      
          def has_publish_permission(self, request, obj=None):
              return request.user.has_perm("myapp.publish_item")
      ```
      
      ### Action Groups (Dropdown Menus)
      
      Group multiple actions under a dropdown button:
      
      ```python
      actions_list = [
          "primary_action",          # standalone button
          {
              "title": _("More Actions"),
              "variant": ActionVariant.PRIMARY,
              "items": [
                  "action_two",
                  "action_three",
                  "action_four",
              ],
          },
      ]
      
      actions_detail = [
          "main_detail_action",
          {
              "title": _("More"),
              "items": ["detail_action_a", "detail_action_b"],
          },
      ]
      ```
      
      ### Permission System
      
      Two approaches work together:
      
      ```python
      # Method 1: Method-based (custom logic)
      @action(permissions=["can_export"])
      def export_data(self, request):
          pass
      
      def has_can_export_permission(self, request):
          return request.user.groups.filter(name="Exporters").exists()
      
      # Method 2: Django built-in permissions
      @action(permissions=["myapp.export_data", "auth.view_user"])
      def export_with_django_perms(self, request):
          pass
      ```
      
      When multiple permissions are listed, ALL must be satisfied (AND logic).
      
      ### Action with Custom Form
      
      Render an intermediate form page from a detail action:
      
      ```python
      @action(description=_("Action with Form"), url_path="custom-form")
      def action_with_form(self, request, object_id):
          obj = get_object_or_404(self.model, pk=object_id)
      
          class ActionForm(forms.Form):
              note = forms.CharField(widget=UnfoldAdminTextInputWidget)
              date = forms.SplitDateTimeField(widget=UnfoldAdminSplitDateTimeWidget)
      
          form = ActionForm(request.POST or None)
      
          if request.method == "POST" and form.is_valid():
              # process form
              messages.success(request, _("Done."))
              return redirect(reverse_lazy("admin:myapp_mymodel_change", args=[object_id]))
      
          return render(request, "myapp/action_form.html", {
              "form": form,
              "object": obj,
              "title": _("Custom Action"),
              **self.admin_site.each_context(request),
          })
      ```
      
      ### Hide Default Actions
      
      ```python
      class MyAdmin(ModelAdmin):
          actions_list_hide_default = True    # hide "Delete selected" etc.
          actions_detail_hide_default = True
      ```
      
      ### Custom URLs
      
      Register custom URL patterns via `get_urls()`:
      
      ```python
      def get_urls(self):
          return super().get_urls() + [
              path("custom-page/", self.admin_site.admin_view(CustomView.as_view(model_admin=self)), name="custom_page"),
          ]
      ```
      
      ## Display Decorator
      
      ### Imports
      
      ```python
      from unfold.decorators import display
      ```
      
      ### @display Parameters
      
      | Parameter | Type | Description |
      |-----------|------|-------------|
      | `description` | str | Column header text |
      | `ordering` | str | Enable sorting via this field |
      | `boolean` | bool | Render as check/cross icon |
      | `label` | bool/dict | Colored label badge |
      | `header` | bool | Rich header with avatar/initials |
      | `dropdown` | bool | Interactive dropdown menu |
      | `image` | bool | Render as image thumbnail |
      
      ### Label Colors
      
      Map field values to color schemes:
      
      ```python
      @display(description=_("Status"), ordering="status", label={
          "active": "success",      # green
          "pending": "info",        # blue
          "suspended": "warning",   # orange
          "banned": "danger",       # red
      })
      def show_status(self, obj):
          return obj.status
      ```
      
      For generic styling without color mapping:
      
      ```python
      @display(description=_("Code"), label=True)
      def show_code(self, obj):
          return obj.code
      ```
      
      ### Header Display
      
      Returns a list: `[primary_text, secondary_text, badge_text, image_config]`
      
      ```python
      @display(description=_("Employee"), header=True)
      def show_employee(self, obj):
          return [
              obj.full_name,                    # line 1 (bold)
              obj.department,                   # line 2 (muted)
              obj.initials,                     # circular badge
              {
                  "path": obj.photo.url if obj.photo else None,
                  "squared": False,             # circular crop (default)
                  "borderless": True,
                  "width": 24,
                  "height": 24,
              },
          ]
      ```
      
      Any element can be `None` to skip it.
      
      ### Dropdown Display
      
      Returns a dict with items or custom HTML:
      
      ```python
      # List-based dropdown
      @display(description=_("Roles"), dropdown=True)
      def show_roles(self, obj):
          roles = obj.roles.all()
          if not roles:
              return "-"
      
          return {
              "title": f"{roles.count()} roles",
              "striped": True,               # alternating row colors
              "height": 400,                 # fixed height
              "max_height": 200,             # max before scrolling
              "width": 240,                  # custom width
              "items": [
                  {"title": role.name, "link": role.get_admin_url()}
                  for role in roles
              ],
          }
      
      # Custom HTML dropdown
      @display(description=_("Preview"), dropdown=True)
      def show_preview(self, obj):
          return {
              "title": "Preview",
              "content": render_to_string("myapp/preview_dropdown.html", {"obj": obj}),
          }
      ```
      
      ### Boolean Display
      
      ```python
      @display(description=_("Active"), boolean=True)
      def show_active(self, obj):
          return obj.is_active
      ```
      
      ## Filter Classes
      
      ### Installation
      
      ```python
      INSTALLED_APPS = [
          "unfold",
          "unfold.contrib.filters",  # must follow unfold
          # ...
      ]
      ```
      
      ### Important: `list_filter_submit`
      
      Input-based filters (text, numeric, date) require a submit button:
      
      ```python
      class MyAdmin(ModelAdmin):
          list_filter_submit = True   # adds submit button to filter panel
          list_filter_sheet = False   # True = filters in sliding sheet panel
      ```
      
      ### Available Filter Classes
      
      All from `unfold.contrib.filters.admin`:
      
      | Filter Class | Input Type | Use Case |
      |-------------|------------|----------|
      | `TextFilter` | Text input | Custom text search (abstract - subclass it) |
      | `RangeNumericFilter` | Two number inputs | Numeric range (min-max) |
      | `SingleNumericFilter` | One number input | Single numeric value (__gte) |
      | `SliderNumericFilter` | Slider control | Numeric range with slider |
      | `RangeNumericListFilter` | Two number inputs | Numeric range (not tied to model field) |
      | `RangeDateFilter` | Two date pickers | Date range |
      | `RangeDateTimeFilter` | Two datetime pickers | DateTime range |
      | `DropdownFilter` | Select dropdown | Custom dropdown (abstract - subclass it) |
      | `MultipleDropdownFilter` | Multi-select dropdown | Custom multi-select dropdown (abstract) |
      | `ChoicesDropdownFilter` | Select dropdown | CharField with choices |
      | `MultipleChoicesDropdownFilter` | Multi-select dropdown | CharField choices multi-select |
      | `RelatedDropdownFilter` | Select dropdown | ForeignKey selection |
      | `MultipleRelatedDropdownFilter` | Multi-select dropdown | ForeignKey multi-select |
      | `RelatedCheckboxFilter` | Checkbox group | ForeignKey as checkboxes |
      | `ChoicesCheckboxFilter` | Checkbox group | CharField choices as checkboxes |
      | `AllValuesCheckboxFilter` | Checkbox group | All distinct field values |
      | `RadioFilter` | Radio buttons | Custom radio (abstract - subclass it) |
      | `BooleanRadioFilter` | Radio buttons | Boolean field |
      | `ChoicesRadioFilter` | Radio buttons | CharField choices as radios |
      | `CheckboxFilter` | Checkbox group | Custom choices (abstract - subclass it) |
      | `AutocompleteSelectFilter` | Autocomplete single | Related model single search |
      | `AutocompleteSelectMultipleFilter` | Autocomplete multi-select | Related model multi search |
      | `FieldTextFilter` | Text input | Field-based text filter (__icontains) |
      
      ### Usage Patterns
      
      ```python
      from unfold.contrib.filters.admin import (
          TextFilter, RangeNumericFilter, RangeDateFilter, RangeDateTimeFilter,
          SingleNumericFilter, SliderNumericFilter, RangeNumericListFilter,
          DropdownFilter, MultipleDropdownFilter,
          ChoicesDropdownFilter, MultipleChoicesDropdownFilter,
          RelatedDropdownFilter, MultipleRelatedDropdownFilter,
          RelatedCheckboxFilter, ChoicesCheckboxFilter, AllValuesCheckboxFilter,
          BooleanRadioFilter, CheckboxFilter, AutocompleteSelectMultipleFilter,
      )
      
      class MyAdmin(ModelAdmin):
          list_filter_submit = True
          list_filter = [
              # Tuple syntax: (field_name, FilterClass)
              ("price", RangeNumericFilter),
              ("status", ChoicesDropdownFilter),       # dropdown for choices
              ("status", ChoicesCheckboxFilter),        # or checkboxes
              ("created_at", RangeDateFilter),
              ("category", RelatedDropdownFilter),
              ("category", MultipleRelatedDropdownFilter),  # multi-select variant
              ("is_active", BooleanRadioFilter),
              ("tags", AutocompleteSelectMultipleFilter),
              ("rating", SingleNumericFilter),
      
              # Direct class (for custom filters)
              NameSearchFilter,
          ]
      ```
      
      ### Slider Filter with Decimals
      
      ```python
      class PriceSliderFilter(SliderNumericFilter):
          MAX_DECIMALS = 2
          STEP = 0.01
      ```
      
      ## Custom Filters
      
      ### Custom Dropdown Filter
      
      Subclass `DropdownFilter` and implement `lookups()` and `queryset()`:
      
      ```python
      from unfold.contrib.filters.admin import DropdownFilter
      
      class RegionFilter(DropdownFilter):
          title = _("Region")
          parameter_name = "region"
      
          def lookups(self, request, model_admin):
              return [
                  ["north", _("North")],
                  ["south", _("South")],
                  ["east", _("East")],
                  ["west", _("West")],
              ]
      
          def queryset(self, request, queryset):
              if self.value() not in EMPTY_VALUES:
                  return queryset.filter(region=self.value())
              return queryset
      ```
      
      ### Custom Text Filter
      
      Subclass `TextFilter` and implement `queryset()`:
      
      ```python
      from django.core.validators import EMPTY_VALUES
      from unfold.contrib.filters.admin import TextFilter
      
      class FullNameFilter(TextFilter):
          title = _("Full name")
          parameter_name = "fullname"
      
          def queryset(self, request, queryset):
              if self.value() in EMPTY_VALUES:
                  return queryset
              return queryset.filter(
                  Q(first_name__icontains=self.value()) |
                  Q(last_name__icontains=self.value())
              )
      ```
      
      ### Custom Checkbox Filter
      
      Subclass `CheckboxFilter` and implement `lookups()` and `queryset()`:
      
      ```python
      from unfold.contrib.filters.admin import CheckboxFilter
      
      class StatusCheckboxFilter(CheckboxFilter):
          title = _("Status")
          parameter_name = "custom_status"
      
          def lookups(self, request, model_admin):
              return [("active", _("Active")), ("inactive", _("Inactive"))]
      
          def queryset(self, request, queryset):
              if self.value() not in EMPTY_VALUES:
                  return queryset.filter(status__in=self.value())
              return queryset
      ```
      
    • configuration.md 11.9 KB
      # Unfold Configuration Reference
      
      ## Table of Contents
      
      - [UNFOLD Settings Dict](#unfold-settings-dict)
      - [Site Branding](#site-branding)
      - [Sidebar Navigation](#sidebar-navigation)
      - [Tabs](#tabs)
      - [Theming and Colors](#theming-and-colors)
      - [Environment Indicator](#environment-indicator)
      - [Login Page](#login-page)
      - [Command Palette](#command-palette)
      - [Custom Styles and Scripts](#custom-styles-and-scripts)
      - [Language Switcher](#language-switcher)
      - [Complete Settings Example](#complete-settings-example)
      
      ## UNFOLD Settings Dict
      
      All configuration lives in `UNFOLD` dict in Django settings:
      
      ```python
      # settings.py
      from django.templatetags.static import static
      from django.urls import reverse_lazy
      from django.utils.translation import gettext_lazy as _
      
      UNFOLD = {
          # Site identity
          "SITE_TITLE": _("My Admin"),          # <title> tag suffix
          "SITE_HEADER": _("My Admin"),          # sidebar header text
          "SITE_SUBHEADER": _("Admin portal"),   # text below header
          "SITE_URL": "/",                       # header link target
      
          # Icons and logos
          "SITE_SYMBOL": "dashboard",            # Material Symbols icon name
          "SITE_ICON": lambda request: static("img/icon.svg"),
          "SITE_LOGO": lambda request: static("img/logo.svg"),
          "SITE_FAVICONS": [
              {"rel": "icon", "sizes": "32x32", "type": "image/svg+xml", "href": lambda request: static("img/favicon.svg")},
          ],
      
          # Light/dark variants for icon and logo
          # "SITE_ICON": {
          #     "light": lambda request: static("img/icon-light.svg"),
          #     "dark": lambda request: static("img/icon-dark.svg"),
          # },
      
          # UI toggles
          "SHOW_HISTORY": True,          # history button on change form
          "SHOW_VIEW_ON_SITE": True,     # "view on site" button
          "SHOW_BACK_BUTTON": False,     # back button on change forms
      
          # Theme
          "THEME": None,                 # None (auto), "dark", or "light"
          "BORDER_RADIUS": "6px",        # global border radius
      
          # Colors (OKLCH format)
          "COLORS": {
              "base": {
                  "50": "250 250 250",
                  "100": "244 245 245",
                  # ... full scale 50-950
                  "950": "10 10 10",
              },
              "primary": {
                  "50": "250 245 255",
                  "100": "243 232 255",
                  # ... full scale 50-950
                  "950": "59 7 100",
              },
              "font": {
                  "subtle-light": "var(--color-base-500)",
                  "subtle-dark": "var(--color-base-400)",
                  "default-light": "var(--color-base-700)",
                  "default-dark": "var(--color-base-200)",
                  "important-light": "var(--color-base-900)",
                  "important-dark": "var(--color-base-100)",
              },
          },
      
          # Callbacks
          "ENVIRONMENT": "myapp.utils.environment_callback",
          "ENVIRONMENT_TITLE_PREFIX": None,
          "DASHBOARD_CALLBACK": "myapp.views.dashboard_callback",
      
          # Login page
          "LOGIN": {
              "image": lambda request: static("img/login-bg.jpg"),
              "redirect_after": "/admin/",
              "form": "myapp.forms.LoginForm",
          },
      
          # Custom assets
          "STYLES": [lambda request: static("css/custom.css")],
          "SCRIPTS": [lambda request: static("js/custom.js")],
      
          # Language
          "SHOW_LANGUAGES": False,
          "LANGUAGE_FLAGS": {"en": "US", "de": "DE"},
      
          # Sidebar
          "SIDEBAR": { ... },   # see below
      
          # Tabs
          "TABS": [ ... ],      # see below
      
          # Command palette
          "COMMAND": { ... },    # see below
      }
      ```
      
      ## Site Branding
      
      ### Icons
      
      Use [Material Symbols](https://fonts.google.com/icons) names for `SITE_SYMBOL` and all navigation icons.
      
      ```python
      "SITE_SYMBOL": "speed",  # displayed in sidebar when logo not set
      ```
      
      ### Logo vs Icon
      
      - `SITE_LOGO` - larger logo for sidebar header
      - `SITE_ICON` - smaller icon (fallback when no logo)
      - Both accept a callable `lambda request: url_string` or a dict with `light`/`dark` keys
      
      ### Site Dropdown
      
      Add links to the site header dropdown:
      
      ```python
      "SITE_DROPDOWN": [
          {
              "icon": "description",
              "title": _("Documentation"),
              "link": "https://docs.example.com",
          },
          {
              "icon": "code",
              "title": _("API Reference"),
              "link": "/api/docs/",
          },
      ],
      ```
      
      ## Sidebar Navigation
      
      ```python
      "SIDEBAR": {
          "show_search": True,               # search bar in sidebar
          "show_all_applications": True,      # "All Applications" link
          "command_search": True,             # command palette integration
          "navigation": [
              {
                  "title": _("Main"),
                  "collapsible": False,       # default: not collapsible
                  "items": [
                      {
                          "title": _("Dashboard"),
                          "icon": "dashboard",
                          "link": reverse_lazy("admin:index"),
                      },
                      {
                          "title": _("Users"),
                          "icon": "people",
                          "link": reverse_lazy("admin:auth_user_changelist"),
                          "badge": "myapp.utils.users_badge",    # callable returning badge text
                          "badge_variant": "danger",              # primary|success|info|warning|danger
                          "badge_style": "solid",                 # solid|outline
                          "permission": "myapp.utils.perm_check", # callable(request) -> bool
                      },
                      {
                          "title": _("Products"),
                          "icon": "inventory_2",
                          "active": "myapp.utils.products_active_callback",  # custom active logic
                          "items": [  # nested subitems
                              {
                                  "title": _("All Products"),
                                  "link": reverse_lazy("admin:shop_product_changelist"),
                              },
                              {
                                  "title": _("Categories"),
                                  "link": reverse_lazy("admin:shop_category_changelist"),
                              },
                          ],
                      },
                  ],
              },
              {
                  "title": _("Settings"),
                  "collapsible": True,
                  "items": [ ... ],
              },
          ],
      },
      ```
      
      ### Navigation Item Fields
      
      | Field | Type | Description |
      |-------|------|-------------|
      | `title` | str | Display text |
      | `icon` | str | Material Symbols icon name |
      | `link` | str/lazy | URL (use `reverse_lazy`) |
      | `badge` | str/callable | Badge text or `"dotted_path.to.callback"` |
      | `badge_variant` | str | primary, success, info, warning, danger |
      | `badge_style` | str | solid, outline |
      | `permission` | str/callable | `"dotted_path.to.callback"` returning bool |
      | `active` | str/callable | Custom active state logic |
      | `items` | list | Nested sub-navigation items |
      | `collapsible` | bool | Group-level: allow collapse |
      
      ### Badge Callback
      
      ```python
      def users_badge(request):
          return User.objects.filter(is_active=False).count() or None
      ```
      
      ### Permission Callback
      
      ```python
      def permission_callback(request):
          return request.user.has_perm("myapp.view_sensitive")
      ```
      
      ### Active Callback
      
      ```python
      def products_active_callback(request):
          return request.path.startswith("/admin/shop/")
      ```
      
      ## Tabs
      
      Tabs appear above the changelist, linking related models or filtered views:
      
      ```python
      "TABS": [
          {
              "models": [
                  "shop.product",
                  "shop.category",
                  {"name": "shop.order", "detail": True},  # show tabs on detail/change pages too
              ],
              "items": [
                  {
                      "title": _("Products"),
                      "link": reverse_lazy("admin:shop_product_changelist"),
                  },
                  {
                      "title": _("Categories"),
                      "link": reverse_lazy("admin:shop_category_changelist"),
                  },
              ],
          },
          {
              "page": "users",  # custom page identifier
              "models": ["auth.user"],
              "items": [
                  {
                      "title": _("All Users"),
                      "link": reverse_lazy("admin:auth_user_changelist"),
                      "active": lambda request: "status" not in request.GET,
                  },
                  {
                      "title": _("Active"),
                      "link": lambda request: f"{reverse_lazy('admin:auth_user_changelist')}?is_active__exact=1",
                  },
                  {
                      "title": _("Staff"),
                      "link": lambda request: f"{reverse_lazy('admin:auth_user_changelist')}?is_staff__exact=1",
                  },
              ],
          },
      ],
      ```
      
      ### Tab Model Formats
      
      Models in the `models` list accept two formats:
      
      ```python
      "models": [
          "app.model",                          # string: tabs on changelist only
          {"name": "app.model", "detail": True},  # dict: also show tabs on change form
      ]
      ```
      
      ### Tab Item Fields
      
      | Field | Type | Description |
      |-------|------|-------------|
      | `title` | str | Tab label |
      | `link` | str/callable | URL or `lambda request: url` |
      | `permission` | str/callable | Visibility control |
      | `active` | callable | `lambda request: bool` for custom active state |
      
      ## Theming and Colors
      
      ### Color System
      
      Unfold uses OKLCH color space with a 50-950 scale. Override `base` and `primary` palettes:
      
      ```python
      "COLORS": {
          "base": {
              "50": "250 250 250",
              "100": "245 245 245",
              "200": "229 229 229",
              "300": "212 212 212",
              "400": "163 163 163",
              "500": "115 115 115",
              "600": "82 82 82",
              "700": "64 64 64",
              "800": "38 38 38",
              "900": "23 23 23",
              "950": "10 10 10",
          },
          "primary": {
              "50": "238 242 255",
              "100": "224 231 255",
              "200": "199 210 254",
              "300": "165 180 252",
              "400": "129 140 248",
              "500": "99 102 241",
              "600": "79 70 229",
              "700": "67 56 202",
              "800": "55 48 163",
              "900": "49 46 129",
              "950": "30 27 75",
          },
      },
      ```
      
      ### Border Radius
      
      ```python
      "BORDER_RADIUS": "6px",   # applies globally
      ```
      
      ### Forced Theme
      
      ```python
      "THEME": "dark",   # disables the light/dark toggle
      ```
      
      ## Environment Indicator
      
      Display a colored badge in the header (e.g., "Development", "Staging"):
      
      ```python
      "ENVIRONMENT": "myapp.utils.environment_callback",
      ```
      
      Callback implementation:
      
      ```python
      def environment_callback(request):
          """Return (name, color_type) or None."""
          if settings.DEBUG:
              return _("Development"), "danger"    # red badge
          if "staging" in request.get_host():
              return _("Staging"), "warning"       # orange badge
          return None  # no badge in production
      ```
      
      Color types: `"danger"`, `"warning"`, `"info"`, `"success"`.
      
      Optional title prefix:
      
      ```python
      "ENVIRONMENT_TITLE_PREFIX": _("DEV"),  # prepends to <title>
      ```
      
      ## Login Page
      
      ```python
      "LOGIN": {
          "image": lambda request: static("img/login-bg.jpg"),
          "redirect_after": "/admin/",
          "form": "myapp.forms.LoginForm",  # custom form class
      },
      ```
      
      ## Command Palette
      
      Configure the Ctrl+K / Cmd+K command palette:
      
      ```python
      "COMMAND": {
          "search_models": True,           # Default: False. Also accepts list/tuple or callback
          # "search_models": ["myapp.mymodel"],           # specific models
          # "search_models": "myapp.utils.search_models_callback",  # callback
          "search_callback": "myapp.utils.search_callback",  # custom search hook
          "show_history": True,            # command history in localStorage
      },
      ```
      
      ### Custom Search Callback
      
      ```python
      from unfold.dataclasses import SearchResult
      
      def search_callback(request, search_term):
          return [
              SearchResult(
                  title="Some title",
                  description="Extra content",
                  link="https://example.com",
                  icon="database",
              )
          ]
      ```
      
      ## Custom Styles and Scripts
      
      Load additional CSS/JS files:
      
      ```python
      "STYLES": [
          lambda request: static("css/admin-overrides.css"),
      ],
      "SCRIPTS": [
          lambda request: static("js/admin-charts.js"),
      ],
      ```
      
      ## Language Switcher
      
      ```python
      "SHOW_LANGUAGES": True,
      "LANGUAGE_FLAGS": {
          "en": "US",
          "de": "DE",
          "fr": "FR",
      },
      ```
      
      ## Complete Settings Example
      
      See the [Formula demo settings](https://github.com/unfoldadmin/formula/blob/main/formula/settings.py) for a production-ready reference.
      
    • dashboard.md 10.1 KB
      # Dashboard, Sections, and Datasets Reference
      
      ## Table of Contents
      
      - [Dashboard Components](#dashboard-components)
      - [Dashboard Callback](#dashboard-callback)
      - [Sections (Changelist Panels)](#sections)
      - [Datasets (Change Form Panels)](#datasets)
      - [Template Injection](#template-injection)
      - [Custom Dashboard Templates](#custom-dashboard-templates)
      
      ## Dashboard Components
      
      ### Imports
      
      ```python
      from unfold.components import BaseComponent, register_component
      from django.template.loader import render_to_string
      ```
      
      ### Creating a Component
      
      Components are registered globally and rendered on the admin index page:
      
      ```python
      @register_component
      class ActiveUsersKPI(BaseComponent):
          def get_context_data(self, **kwargs):
              context = super().get_context_data(**kwargs)
              context["children"] = render_to_string("myapp/kpi_card.html", {
                  "total": User.objects.filter(is_active=True).count(),
                  "label": "Active Users",
                  "progress": "positive",    # or "negative"
                  "percentage": "+5.2%",
              })
              return context
      ```
      
      ### KPI Card Template Pattern
      
      ```html
      <!-- templates/myapp/kpi_card.html -->
      <div class="flex flex-col gap-1">
          <div class="text-2xl font-bold text-base-900 dark:text-base-100">
              {{ total }}
          </div>
          <div class="flex items-center gap-2">
              <span class="text-sm text-base-500 dark:text-base-400">{{ label }}</span>
              {% if percentage %}
              <span class="text-xs {% if progress == 'positive' %}text-green-600{% else %}text-red-600{% endif %}">
                  {{ percentage }}
              </span>
              {% endif %}
          </div>
      </div>
      ```
      
      ### Chart Component
      
      Unfold supports Chart.js via custom components:
      
      ```python
      @register_component
      class SalesChartComponent(BaseComponent):
          def get_context_data(self, **kwargs):
              context = super().get_context_data(**kwargs)
              context["data"] = json.dumps({
                  "labels": ["Mon", "Tue", "Wed", "Thu", "Fri"],
                  "datasets": [{
                      "data": [[1, 5], [1, 8], [1, 12], [1, 7], [1, 15]],
                      "backgroundColor": "var(--color-primary-600)",
                  }],
              })
              return context
      ```
      
      ### Component Rendering
      
      Components are rendered in the admin index template. Customize the index template to control layout:
      
      ```html
      <!-- templates/admin/index.html -->
      {% extends "unfold/layouts/base_simple.html" %}
      {% load unfold %}
      
      {% block content %}
      <div class="grid grid-cols-1 lg:grid-cols-4 gap-4 mb-8">
          {% component "ActiveUsersKPI" %}{% endcomponent %}
          {% component "RevenueKPI" %}{% endcomponent %}
          {% component "OrdersKPI" %}{% endcomponent %}
          {% component "ConversionKPI" %}{% endcomponent %}
      </div>
      <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
          {% component "SalesChartComponent" %}{% endcomponent %}
          {% component "RecentOrdersComponent" %}{% endcomponent %}
      </div>
      {% endblock %}
      ```
      
      ## Dashboard Callback
      
      Configure in settings:
      
      ```python
      UNFOLD = {
          "DASHBOARD_CALLBACK": "myapp.views.dashboard_callback",
      }
      ```
      
      The callback prepares template context for the admin index:
      
      ```python
      # myapp/views.py
      def dashboard_callback(request, context):
          """Add extra context to admin dashboard."""
          context.update({
              "custom_variable": "value",
              "stats": get_dashboard_stats(),
          })
          return context
      ```
      
      ## Sections
      
      Sections are panels displayed below the changelist table.
      
      ### Imports
      
      ```python
      from unfold.sections import TableSection, TemplateSection
      ```
      
      ### TableSection
      
      Renders a related model's data as a table:
      
      ```python
      class RecentOrdersSection(TableSection):
          related_name = "order_set"      # related manager name
          fields = ["id", "total", "status", "custom_field"]
          height = 380                     # fixed height in px
      
          @admin.display(description=_("Formatted Total"))
          def custom_field(self, instance):
              return f"${instance.total:.2f}"
      ```
      
      ### TemplateSection
      
      Renders a custom template:
      
      ```python
      class AnalyticsSection(TemplateSection):
          template_name = "myapp/analytics_chart.html"
      ```
      
      ### Registering Sections
      
      ```python
      class MyAdmin(ModelAdmin):
          list_sections = [RecentOrdersSection, AnalyticsSection]
          list_sections_classes = "lg:grid-cols-2"  # CSS grid layout
      ```
      
      ## Datasets
      
      Datasets embed full mini-admin listings within change forms. Think of them as "related admin views" rendered inside another model's edit page.
      
      ### Imports
      
      ```python
      from unfold.datasets import BaseDataset
      from unfold.admin import ModelAdmin
      ```
      
      ### Creating a Dataset
      
      ```python
      # Step 1: Define a mini-admin for the dataset
      class RelatedItemDatasetAdmin(ModelAdmin):
          list_display = ["name", "status", "created_at"]
          search_fields = ["name"]
          actions = ["bulk_approve"]
      
          def bulk_approve(self, request, queryset):
              queryset.update(status="approved")
              messages.success(request, "Approved.")
              return redirect(request.headers.get("referer"))
      
          def get_queryset(self, request):
              obj = self.extra_context.get("object")  # parent object
              if not obj:
                  return super().get_queryset(request).none()
              return super().get_queryset(request).filter(parent=obj)
      
      # Step 2: Define the dataset
      class RelatedItemDataset(BaseDataset):
          model = RelatedItem
          model_admin = RelatedItemDatasetAdmin
          tab = True  # render as tab on change form
      
      # Step 3: Register on the parent admin
      class ParentAdmin(ModelAdmin):
          change_form_datasets = [RelatedItemDataset]
      ```
      
      ### Dataset Fields
      
      | Field | Type | Description |
      |-------|------|-------------|
      | `model` | Model class | The related model |
      | `model_admin` | ModelAdmin class | Mini-admin configuration |
      | `tab` | bool | Show as tab on change form |
      
      ### Key Pattern: Access Parent Object
      
      In the dataset's `model_admin`, access the parent object via `self.extra_context`:
      
      ```python
      def get_queryset(self, request):
          obj = self.extra_context.get("object")
          if not obj:
              return super().get_queryset(request).none()
          return super().get_queryset(request).filter(owner=obj)
      ```
      
      ## Template Injection
      
      ### Changelist Templates
      
      ```python
      class MyAdmin(ModelAdmin):
          list_before_template = "myapp/list_before.html"  # above table
          list_after_template = "myapp/list_after.html"     # below table
      ```
      
      ### Change Form Templates
      
      ```python
      class MyAdmin(ModelAdmin):
          change_form_before_template = "myapp/form_before.html"  # above form
          change_form_after_template = "myapp/form_after.html"     # below form
      ```
      
      ### Template Context
      
      Injected templates receive the standard Django admin template context plus the Unfold context. Use `{{ cl }}` for changelist context, `{{ original }}` for the object in change form templates.
      
      ```html
      <!-- templates/myapp/list_before.html -->
      <div class="rounded-lg border border-base-200 dark:border-base-700 p-4 mb-4">
          <h3 class="text-lg font-semibold text-base-900 dark:text-base-100">
              Quick Stats
          </h3>
          <p class="text-base-500 dark:text-base-400">
              Showing {{ cl.result_count }} results
          </p>
      </div>
      ```
      
      ## Custom Dashboard Templates
      
      ### Override admin/index.html
      
      ```html
      <!-- templates/admin/index.html -->
      {% extends "unfold/layouts/base_simple.html" %}
      {% load i18n unfold %}
      
      {% block title %}
          {{ title }} | {{ site_title }}
      {% endblock %}
      
      {% block content %}
          {% include "myapp/dashboards.html" %}
      {% endblock %}
      ```
      
      ### Dashboard Layout Pattern
      
      ```html
      <!-- templates/myapp/dashboards.html -->
      {% load unfold %}
      
      <!-- KPI Row -->
      <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
          {% component "KPI1" %}{% endcomponent %}
          {% component "KPI2" %}{% endcomponent %}
          {% component "KPI3" %}{% endcomponent %}
          {% component "KPI4" %}{% endcomponent %}
      </div>
      
      <!-- Charts Row -->
      <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
          {% component "Chart1" %}{% endcomponent %}
          {% component "Chart2" %}{% endcomponent %}
      </div>
      
      <!-- Tables Row -->
      <div class="grid grid-cols-1 gap-4">
          {% component "RecentActivity" %}{% endcomponent %}
      </div>
      ```
      
      ### Tailwind CSS Classes
      
      Unfold uses Tailwind CSS. Common utility patterns for custom templates:
      
      | Pattern | Classes |
      |---------|---------|
      | Card | `rounded-lg border border-base-200 dark:border-base-700 bg-white dark:bg-base-900 p-4` |
      | Heading | `text-lg font-semibold text-base-900 dark:text-base-100` |
      | Muted text | `text-sm text-base-500 dark:text-base-400` |
      | Grid layout | `grid grid-cols-1 lg:grid-cols-2 gap-4` |
      | Flex row | `flex items-center gap-2` |
      
      ### Paginator
      
      ```python
      from unfold.paginator import InfinitePaginator
      
      class MyAdmin(ModelAdmin):
          paginator = InfinitePaginator
          show_full_result_count = False
          list_per_page = 20
      ```
      
      `InfinitePaginator` provides infinite scroll instead of page numbers.
      
      ## Component Data Formats
      
      ### Cohort Data
      
      ```python
      cohort_data = {
          "headers": [
              {"title": "Week 1", "subtitle": "Jan 1-7"},
              {"title": "Week 2", "subtitle": "Jan 8-14"},
          ],
          "rows": [
              {
                  "header": {"title": "Cohort A", "subtitle": "100 users"},
                  "cols": [
                      {"value": "85%", "subtitle": "85 users"},
                      {"value": "72%", "subtitle": "72 users"},
                  ],
              },
          ],
      }
      ```
      
      ### Tracker Data
      
      ```python
      tracker_data = [
          {"color": "bg-primary-400 dark:bg-primary-700", "tooltip": "Jan 1: 5 events"},
          {"color": "bg-primary-200 dark:bg-primary-900", "tooltip": "Jan 2: 2 events"},
          {"color": "bg-danger-400 dark:bg-danger-700", "tooltip": "Jan 3: 0 events"},
      ]
      ```
      
      ### Progress Data (Multi-Segment)
      
      ```python
      # Single bar
      progress_single = {
          "title": "Completion",
          "description": "57.5%",
          "value": 57.5,
      }
      
      # Multi-segment bar
      progress_multi = {
          "title": "Distribution",
          "description": "Total 100%",
          "items": [
              {"title": "Active", "value": 60.0, "progress-class": "bg-primary-500"},
              {"title": "Pending", "value": 25.0, "progress-class": "bg-warning-500"},
              {"title": "Inactive", "value": 15.0, "progress-class": "bg-danger-500"},
          ],
      }
      ```
      
      ### Table Data
      
      ```python
      table_data = {
          "headers": ["Name", "Status", "Amount"],
          "rows": [
              ["John", "Active", "$500"],
              ["Jane", "Inactive", "$300"],
          ],
          "collapsible": True,  # optional
      }
      ```
      
    • resources.md 17 KB
      # Resources, Integrations, and Patterns
      
      ## Table of Contents
      
      - [Official Resources](#official-resources)
      - [Third-Party Integrations](#third-party-integrations)
      - [Built-In Template Components](#built-in-template-components)
      - [Common Patterns](#common-patterns)
      - [Version Compatibility](#version-compatibility)
      - [Unfold Studio](#unfold-studio)
      - [Community and Learning](#community-and-learning)
      
      ## Official Resources
      
      | Resource | URL |
      |----------|-----|
      | Documentation | https://unfoldadmin.com/docs/ |
      | GitHub Repository | https://github.com/unfoldadmin/django-unfold |
      | PyPI | https://pypi.org/project/django-unfold/ |
      | Live Demo | https://demo.unfoldadmin.com |
      | Formula Demo App | https://github.com/unfoldadmin/formula |
      | Turbo Boilerplate (Django + Next.js) | https://github.com/unfoldadmin/turbo |
      | Material Symbols (Icons) | https://fonts.google.com/icons |
      | Discord Community | https://discord.gg/9sQj9MEbNz |
      | Blog | https://unfoldadmin.com/blog/ |
      | Features Overview | https://unfoldadmin.com/features/ |
      | Django Packages | https://djangopackages.org/packages/p/django-unfold/ |
      
      ### Formula Demo App
      
      The [Formula](https://github.com/unfoldadmin/formula) demo is the authoritative reference implementation. It demonstrates:
      - Every action type (list, row, detail, submit line) with permissions
      - All filter classes with custom filters
      - Display decorators (header, dropdown, label, boolean)
      - Dashboard components with KPI cards and charts
      - Datasets embedded in change forms
      - Sections (TableSection, TemplateSection)
      - Conditional fields, fieldset tabs, inline tabs
      - Third-party integrations (celery-beat, guardian, simple-history, import-export, modeltranslation, crispy-forms, djangoql)
      - Custom form views and URL registration
      - Template injection points
      - InfinitePaginator
      - Nonrelated inlines
      
      When unsure about implementation, consult `formula/admin.py` and `formula/settings.py` in the Formula repo.
      
      ## Third-Party Integrations
      
      Unfold provides styled wrappers for these packages. Use the multiple inheritance pattern:
      
      ### Supported Packages
      
      | Package | Unfold Module | What It Provides |
      |---------|--------------|------------------|
      | django-import-export | `unfold.contrib.import_export` | `ImportForm`, `ExportForm`, `SelectableFieldsExportForm` |
      | django-guardian | `unfold.contrib.guardian` | Styled guardian admin integration |
      | django-simple-history | `unfold.contrib.simple_history` | Styled history admin |
      | django-constance | `unfold.contrib.constance` | Styled constance config admin |
      | django-location-field | `unfold.contrib.location_field` | `UnfoldAdminLocationWidget` |
      | django-celery-beat | Compatible (requires rewiring) | Unregister/re-register all 5 models |
      | django-modeltranslation | Compatible | Mix `TabbedTranslationAdmin` with `ModelAdmin` |
      | django-money | `unfold.widgets` | `UnfoldAdminMoneyWidget` |
      | djangoql | Compatible | Mix `DjangoQLSearchMixin` with `ModelAdmin` |
      | django-json-widget | Compatible | Use Unfold form overrides |
      | django-crispy-forms | Compatible | Unfold template pack `unfold_crispy` |
      
      ### django-import-export Setup
      
      ```python
      from import_export.admin import ImportExportModelAdmin, ExportActionModelAdmin
      from unfold.contrib.import_export.forms import ImportForm, ExportForm, SelectableFieldsExportForm
      
      @admin.register(MyModel)
      class MyModelAdmin(ModelAdmin, ImportExportModelAdmin, ExportActionModelAdmin):
          resource_classes = [MyResource, AnotherResource]
          import_form_class = ImportForm
          export_form_class = SelectableFieldsExportForm  # or ExportForm
      ```
      
      ### django-celery-beat Setup
      
      Requires unregistering and re-registering all celery-beat models:
      
      ```python
      from django_celery_beat.admin import (
          ClockedScheduleAdmin as BaseClockedScheduleAdmin,
          CrontabScheduleAdmin as BaseCrontabScheduleAdmin,
          PeriodicTaskAdmin as BasePeriodicTaskAdmin,
          PeriodicTaskForm, TaskSelectWidget,
      )
      from django_celery_beat.models import (
          ClockedSchedule, CrontabSchedule, IntervalSchedule,
          PeriodicTask, SolarSchedule,
      )
      
      admin.site.unregister(PeriodicTask)
      admin.site.unregister(IntervalSchedule)
      admin.site.unregister(CrontabSchedule)
      admin.site.unregister(SolarSchedule)
      admin.site.unregister(ClockedSchedule)
      
      # Merge TaskSelectWidget with Unfold's select
      class UnfoldTaskSelectWidget(UnfoldAdminSelectWidget, TaskSelectWidget):
          pass
      
      class UnfoldPeriodicTaskForm(PeriodicTaskForm):
          def __init__(self, *args, **kwargs):
              super().__init__(*args, **kwargs)
              self.fields["task"].widget = UnfoldAdminTextInputWidget()
              self.fields["regtask"].widget = UnfoldTaskSelectWidget()
      
      @admin.register(PeriodicTask)
      class PeriodicTaskAdmin(BasePeriodicTaskAdmin, ModelAdmin):
          form = UnfoldPeriodicTaskForm
      
      @admin.register(IntervalSchedule)
      class IntervalScheduleAdmin(ModelAdmin):
          pass
      
      @admin.register(CrontabSchedule)
      class CrontabScheduleAdmin(BaseCrontabScheduleAdmin, ModelAdmin):
          pass
      
      @admin.register(SolarSchedule)
      class SolarScheduleAdmin(ModelAdmin):
          pass
      
      @admin.register(ClockedSchedule)
      class ClockedScheduleAdmin(BaseClockedScheduleAdmin, ModelAdmin):
          pass
      ```
      
      ### django-crispy-forms Setup
      
      ```python
      # settings.py
      CRISPY_TEMPLATE_PACK = "unfold_crispy"
      CRISPY_ALLOWED_TEMPLATE_PACKS = ["unfold_crispy"]
      ```
      
      In templates: `{% crispy form "unfold_crispy" %}`. For formsets, use `"unfold_crispy/layout/table_inline_formset.html"` as the FormHelper template.
      
      ### django-constance Setup
      
      ```python
      INSTALLED_APPS = [
          "unfold.contrib.constance",
          "constance",
      ]
      ```
      
      Use `UNFOLD_CONSTANCE_ADDITIONAL_FIELDS` from `unfold.contrib.constance.settings` to define custom field widgets for constance config values.
      
      ### Multiple Inheritance Pattern
      
      Order matters - Unfold `ModelAdmin` should be last:
      
      ```python
      # Correct order: specific mixins first, Unfold ModelAdmin last
      @admin.register(MyModel)
      class MyModelAdmin(DjangoQLSearchMixin, SimpleHistoryAdmin, GuardedModelAdmin, ModelAdmin):
          pass
      ```
      
      ## Built-In Template Components
      
      Unfold ships with reusable template components for dashboards and custom pages. Use with Django's `{% include %}` tag:
      
      ### Component Templates
      
      | Component | Template Path | Key Variables |
      |-----------|--------------|---------------|
      | Button | `unfold/components/button.html` | `class`, `name`, `href`, `submit` |
      | Card | `unfold/components/card.html` | `class`, `title`, `footer`, `label`, `icon` |
      | Bar Chart | `unfold/components/chart/bar.html` | `class`, `data` (JSON), `height`, `width` |
      | Line Chart | `unfold/components/chart/line.html` | `class`, `data` (JSON), `height`, `width` |
      | Cohort | `unfold/components/cohort.html` | `data` |
      | Container | `unfold/components/container.html` | `class` |
      | Flex | `unfold/components/flex.html` | `class`, `col` |
      | Icon | `unfold/components/icon.html` | `class` |
      | Navigation | `unfold/components/navigation.html` | `class`, `items` |
      | Progress | `unfold/components/progress.html` | `class`, `value`, `title`, `description` |
      | Separator | `unfold/components/separator.html` | `class` |
      | Table | `unfold/components/table.html` | `table`, `card_included`, `striped` |
      | Text | `unfold/components/text.html` | `class` |
      | Title | `unfold/components/title.html` | `class` |
      | Tracker | `unfold/components/tracker.html` | `class`, `data` |
      | Layer | `unfold/components/layer.html` | Wrapper component |
      
      ### Using Components in Templates
      
      ```html
      {% load unfold %}
      
      {# KPI Card #}
      {% component "MyKPIComponent" %}{% endcomponent %}
      
      {# Include with variables #}
      {% include "unfold/components/card.html" with title="Revenue" icon="payments" %}
          <div class="text-2xl font-bold">$42,000</div>
      {% endinclude %}
      
      {# Chart with JSON data #}
      {% include "unfold/components/chart/bar.html" with data=chart_data height=300 %}
      ```
      
      ### Chart Data Format (Chart.js)
      
      ```python
      import json
      
      chart_data = json.dumps({
          "labels": ["Jan", "Feb", "Mar", "Apr"],
          "datasets": [{
              "label": "Revenue",
              "data": [4000, 5200, 4800, 6100],
              "backgroundColor": "var(--color-primary-600)",
          }],
      })
      ```
      
      ## Common Patterns
      
      ### Pattern 1: Proxy Model for Alternate Views
      
      Use Django proxy models to create different admin views of the same data:
      
      ```python
      # models.py
      class ActiveUser(User):
          class Meta:
              proxy = True
      
      # admin.py
      @admin.register(ActiveUser)
      class ActiveUserAdmin(ModelAdmin):
          def get_queryset(self, request):
              return super().get_queryset(request).filter(is_active=True)
      ```
      
      ### Pattern 2: Custom Admin Site
      
      ```python
      # sites.py
      from unfold.sites import UnfoldAdminSite
      
      class MyAdminSite(UnfoldAdminSite):
          site_header = "My Admin"
          site_title = "My Admin"
      
      admin_site = MyAdminSite(name="myadmin")
      
      # urls.py
      urlpatterns = [
          path("admin/", admin_site.urls),
      ]
      ```
      
      ### Pattern 3: Optimized Querysets
      
      Always optimize querysets for list views with annotations and prefetches:
      
      ```python
      def get_queryset(self, request):
          return (
              super().get_queryset(request)
              .annotate(total_points=Sum("standing__points"))
              .select_related("author", "category")
              .prefetch_related("tags", "teams")
          )
      ```
      
      ### Pattern 4: Conditional Registration
      
      Conditionally register admin classes based on installed apps:
      
      ```python
      if "django_celery_beat" in settings.INSTALLED_APPS:
          @admin.register(PeriodicTask)
          class PeriodicTaskAdmin(BasePeriodicTaskAdmin, ModelAdmin):
              pass
      ```
      
      ### Pattern 5: Dynamic Sidebar Badges
      
      ```python
      # utils.py
      def pending_orders_badge(request):
          count = Order.objects.filter(status="pending").count()
          return str(count) if count > 0 else None
      ```
      
      ```python
      # settings.py
      "SIDEBAR": {
          "navigation": [{
              "items": [{
                  "title": "Orders",
                  "badge": "myapp.utils.pending_orders_badge",
                  "badge_variant": "danger",
              }],
          }],
      }
      ```
      
      ### Pattern 6: Environment-Aware Configuration
      
      ```python
      def environment_callback(request):
          if settings.DEBUG:
              return _("Development"), "danger"
          host = request.get_host()
          if "staging" in host:
              return _("Staging"), "warning"
          if "demo" in host:
              return _("Demo"), "info"
          return None  # production - no badge
      ```
      
      ### Pattern 7: Admin Actions with Intermediate Forms
      
      For actions that need user input before executing:
      
      ```python
      @action(description=_("Schedule Export"), url_path="schedule-export")
      def schedule_export(self, request, object_id):
          obj = get_object_or_404(self.model, pk=object_id)
      
          class ExportForm(forms.Form):
              format = forms.ChoiceField(
                  choices=[("csv", "CSV"), ("xlsx", "Excel")],
                  widget=UnfoldAdminSelectWidget,
              )
              date_range = forms.SplitDateTimeField(
                  widget=UnfoldAdminSplitDateTimeWidget,
                  required=False,
              )
      
          form = ExportForm(request.POST or None)
          if request.method == "POST" and form.is_valid():
              # schedule export task
              messages.success(request, "Export scheduled.")
              return redirect(reverse_lazy("admin:myapp_mymodel_change", args=[object_id]))
      
          return render(request, "myapp/export_form.html", {
              "form": form,
              "object": obj,
              "title": f"Schedule Export for {obj}",
              **self.admin_site.each_context(request),
          })
      ```
      
      ### Pattern 8: Full-Width Changelist with Sheet Filters
      
      ```python
      class MyAdmin(ModelAdmin):
          list_fullwidth = True       # no sidebar, full width
          list_filter_submit = True   # submit button
          list_filter_sheet = True    # filters in sliding sheet panel
      ```
      
      ### Pattern 9: Sortable Model with Hidden Weight Field
      
      ```python
      # models.py
      class MenuItem(models.Model):
          name = models.CharField(max_length=100)
          weight = models.PositiveIntegerField(default=0)
      
          class Meta:
              ordering = ["weight"]
      
      # admin.py
      class MenuItemAdmin(ModelAdmin):
          ordering_field = "weight"
          hide_ordering_field = True  # hides weight column from list_display
      ```
      
      ## Version Compatibility
      
      | django-unfold | Python | Django |
      |---------------|--------|--------|
      | 0.78.x (latest) | >=3.10, <4.0 | 4.2, 5.0, 5.1, 5.2, 6.0 |
      
      ### Required INSTALLED_APPS Order
      
      ```python
      INSTALLED_APPS = [
          # Unfold MUST come before django.contrib.admin
          "unfold",
          "unfold.contrib.filters",     # optional
          "unfold.contrib.forms",       # optional
          "unfold.contrib.inlines",     # optional
          "unfold.contrib.import_export",  # optional
          # Then Django
          "django.contrib.admin",
          "django.contrib.auth",
          # ...
      ]
      ```
      
      ## Unfold Studio
      
      Unfold Studio is the commercial offering built on top of the open-source django-unfold:
      
      - **Pre-built dashboard templates** - ready-made KPI layouts
      - **Additional components** - extended component library
      - **Studio settings** - `UNFOLD["STUDIO"]` with options like `header_sticky`, `layout_style` (boxed), `header_variant`, `sidebar_style` (minimal), `sidebar_variant`, `site_banner`
      
      Studio settings (all optional, only available with Studio license):
      
      ```python
      UNFOLD = {
          "STUDIO": {
              "header_sticky": True,
              "layout_style": "boxed",
              "header_variant": "dark",
              "sidebar_style": "minimal",
              "sidebar_variant": "dark",
              "site_banner": "Important announcement",
          },
      }
      ```
      
      ## Community and Learning
      
      ### Official Blog Posts
      
      | Title | Topic |
      |-------|-------|
      | Making Django admin models readonly with custom mixins | Readonly patterns |
      | Migrating existing Django admin interface to Unfold | Migration guide |
      | Creating modal windows with Alpine.js and HTMX | UI techniques |
      | Turn Django admin into full-fledged dashboard with Unfold | Dashboard setup |
      | Dark mode toggle in Django with TailwindCSS and Alpine.js | Theming |
      | Configuring custom AWS S3 storage backends in django-storages | Storage |
      | Customizing and loading your own Django admin site | Custom admin sites |
      | Setting up a new Django project with Poetry and Docker | Project setup |
      
      All at https://unfoldadmin.com/blog/
      
      ### Community Tutorials
      
      | Title | Author | URL |
      |-------|--------|-----|
      | Django Unfold Tutorial: A Better Admin Panel | Bastiaan Rudolf | https://medium.com/@bastiaanrudolf/django-unfold-tutorial-a-better-admin-panel-e0b36e03e653 |
      | Getting Started with Django Unfold | Mehedi Khan | https://medium.com/django-unleashed/getting-started-with-django-unfold-a-modern-ui-for-django-admin-aeb8be63bd0a |
      | Simplify Your Django Admin with django-unfold | eshat002 | https://dev.to/eshat002/simplify-your-django-admin-with-django-unfold-5g16 |
      | Custom Django Unfold Admin Dashboard | thalida | https://www.thalida.com/guides/post/custom-django-unfold-admin-dashboard |
      
      ### Other References
      
      - **Formula demo walkthrough**: Study `formula/admin.py` for real-world patterns
      - **GitHub Discussions**: https://github.com/unfoldadmin/django-unfold/discussions
      - **Django Admin Theme Roundup**: https://www.djangoproject.com/weblog/2025/apr/18/admin-theme-roundup/
      - **RTL Support**: https://pypi.org/project/django-unfold-rtl/ (community fork)
      
      ### Tips from the Community
      
      1. **Always use `list_filter_submit = True`** when using input-based filters (text, numeric, date) - without it, filters trigger on every keystroke
      2. **Prefetch/select_related in get_queryset** - Unfold's rich display decorators (header, dropdown) make this critical for performance
      3. **Use `compressed_fields = True`** for dense forms - reduces vertical space significantly
      4. **Action permissions use AND logic** - all listed permissions must be satisfied
      5. **InfinitePaginator + show_full_result_count=False** - recommended for large datasets
      6. **Tab ordering follows fieldset/inline order** - fieldset tabs appear first, then inline tabs
      7. **Conditional fields use Alpine.js expressions** - field names map directly to form field names
      8. **Sidebar badge callbacks are called on every request** - keep them fast, consider caching
      9. **Unfold ModelAdmin must come LAST** in multiple inheritance - `class MyAdmin(MixinA, MixinB, ModelAdmin):`
      10. **`formfield_overrides` are automatic** - Unfold maps all standard Django fields to styled widgets by default; only override when you need a *different* widget (e.g., switch, WYSIWYG)
      11. **`list_filter_sheet = True` requires `list_filter_submit = True`** - the sheet panel needs the submit button to function
      12. **Nonrelated inlines require `unfold.contrib.inlines`** in INSTALLED_APPS - forgetting this is a common source of import errors
      13. **Action `url_path` must be unique** per admin class - duplicate paths cause silent routing failures
      14. **`readonly_preprocess_fields`** accepts callables like `{"field": "html"}` to render HTML in readonly fields, or custom functions
      15. **`add_fieldsets`** attribute works like Django's UserAdmin - define separate fieldsets for the add form vs. the change form
      16. **Fieldset tabs + `gettext_lazy`**: Tab CSS class detection can break when combined with lazy translation - test tab switching after adding i18n
      17. **Dataset pagination resets focus**: Navigating paginated datasets inside tabs may jump back to the General tab
      18. **`warn_unsaved_form` + `ordering_field`**: These conflict in TabularInline - unsaved warning fires on drag-to-reorder
      19. **Releases every 2-3 weeks** - pin your version in production; Unfold moves fast and breaking changes do occur between minor versions
      
    • widgets-inlines.md 15.7 KB
      # Widgets, Inlines, and Forms Reference
      
      ## Table of Contents
      
      - [Widget Classes](#widget-classes)
      - [Widget Override Patterns](#widget-override-patterns)
      - [Contrib Widgets](#contrib-widgets)
      - [Inline Classes](#inline-classes)
      - [Nonrelated Inlines](#nonrelated-inlines)
      - [Inline Configuration](#inline-configuration)
      - [User Forms](#user-forms)
      - [Custom Form Fields](#custom-form-fields)
      
      ## Widget Classes
      
      All from `unfold.widgets`:
      
      ### Text and Input Widgets
      
      | Widget | Replaces | Notes |
      |--------|----------|-------|
      | `UnfoldAdminTextInputWidget` | `AdminTextInputWidget` | Supports prefix/suffix icons |
      | `UnfoldAdminURLInputWidget` | `AdminURLFieldWidget` | URL-specific styling |
      | `UnfoldAdminEmailInputWidget` | `AdminEmailInputWidget` | Email input |
      | `UnfoldAdminColorInputWidget` | `AdminTextInputWidget` | Color picker (`type="color"`) |
      | `UnfoldAdminUUIDInputWidget` | `AdminUUIDInputWidget` | UUID input |
      | `UnfoldAdminPasswordWidget` | `PasswordInput` | Password with `render_value` param |
      | `UnfoldAdminPasswordToggleWidget` | `PasswordInput` | Password with visibility toggle |
      | `UnfoldAdminIntegerRangeWidget` | `MultiWidget` | Two-input range widget |
      
      ### Textarea Widgets
      
      | Widget | Notes |
      |--------|-------|
      | `UnfoldAdminTextareaWidget` | Standard textarea |
      | `UnfoldAdminExpandableTextareaWidget` | Starts at 2 rows, expands |
      
      ### Number Widgets
      
      | Widget | Replaces |
      |--------|----------|
      | `UnfoldAdminIntegerFieldWidget` | `AdminIntegerFieldWidget` |
      | `UnfoldAdminDecimalFieldWidget` | `AdminIntegerFieldWidget` |
      | `UnfoldAdminBigIntegerFieldWidget` | `AdminBigIntegerFieldWidget` |
      
      ### Select Widgets
      
      | Widget | Notes |
      |--------|-------|
      | `UnfoldAdminSelectWidget` | Basic styled select |
      | `UnfoldAdminSelect2Widget` | Select2 with search (includes jQuery/Select2 JS) |
      | `UnfoldAdminSelectMultipleWidget` | Multi-select |
      | `UnfoldAdminSelect2MultipleWidget` | Select2 multi-select with search |
      | `UnfoldAdminNullBooleanSelectWidget` | Yes/No/Unknown select |
      | `UnfoldAdminRadioSelectWidget` | Radio buttons (VERTICAL default) |
      | `UnfoldAdminCheckboxSelectMultiple` | Checkbox group |
      
      ### Boolean Widgets
      
      | Widget | Notes |
      |--------|-------|
      | `UnfoldBooleanWidget` | Standard checkbox |
      | `UnfoldBooleanSwitchWidget` | Toggle switch |
      
      ### Date/Time Widgets
      
      | Widget | Notes |
      |--------|-------|
      | `UnfoldAdminDateWidget` | Date picker |
      | `UnfoldAdminSingleDateWidget` | Simple date input |
      | `UnfoldAdminTimeWidget` | Time picker |
      | `UnfoldAdminSingleTimeWidget` | Simple time input |
      | `UnfoldAdminSplitDateTimeWidget` | Separate date + time inputs |
      | `UnfoldAdminSplitDateTimeVerticalWidget` | Vertical layout with labels |
      
      ### File Widgets
      
      | Widget | Notes |
      |--------|-------|
      | `UnfoldAdminImageFieldWidget` | Image upload with preview |
      | `UnfoldAdminFileFieldWidget` | File upload (compact) |
      | `UnfoldAdminImageSmallFieldWidget` | Small image upload |
      
      ### Relational Widgets
      
      | Widget | Notes |
      |--------|-------|
      | `UnfoldRelatedFieldWidgetWrapper` | Styled related field wrapper |
      | `UnfoldForeignKeyRawIdWidget` | Raw ID for ForeignKey |
      | `UnfoldAdminAutocompleteWidget` | Autocomplete select |
      | `UnfoldAdminMultipleAutocompleteWidget` | Autocomplete multi-select |
      
      ### Third-Party Integration Widgets
      
      | Widget | Requires |
      |--------|----------|
      | `UnfoldAdminMoneyWidget` | `django-money` |
      | `UnfoldAdminLocationWidget` | `django-location-field` |
      
      ### Autocomplete Field Classes
      
      For custom autocomplete views (beyond Django's built-in `autocomplete_fields`):
      
      ```python
      from unfold.fields import (
          UnfoldAdminAutocompleteModelChoiceField,        # single select
          UnfoldAdminMultipleAutocompleteModelChoiceField, # multi select
      )
      ```
      
      These are form **fields** (not widgets) - use them in custom forms when you need autocomplete with a custom `BaseAutocompleteView` backend.
      
      ## Automatic Widget Mapping
      
      Unfold automatically applies styled widgets to all standard Django fields. You do NOT need `formfield_overrides` for basic field types - they're handled by default. This mapping shows what Unfold applies behind the scenes:
      
      | Django Field | Unfold Widget (auto-applied) |
      |-------------|------------------------------|
      | `CharField` | `UnfoldAdminTextInputWidget` |
      | `TextField` | `UnfoldAdminTextareaWidget` |
      | `IntegerField` | `UnfoldAdminIntegerFieldWidget` |
      | `DecimalField` | `UnfoldAdminDecimalFieldWidget` |
      | `BigIntegerField` | `UnfoldAdminBigIntegerFieldWidget` |
      | `BooleanField` | `UnfoldBooleanWidget` |
      | `NullBooleanField` | `UnfoldAdminNullBooleanSelectWidget` |
      | `DateField` | `UnfoldAdminDateWidget` |
      | `TimeField` | `UnfoldAdminTimeWidget` |
      | `DateTimeField` | `UnfoldAdminSplitDateTimeWidget` |
      | `EmailField` | `UnfoldAdminEmailInputWidget` |
      | `URLField` | `UnfoldAdminURLInputWidget` |
      | `UUIDField` | `UnfoldAdminUUIDInputWidget` |
      | `ForeignKey` | `UnfoldAdminSelectWidget` (or autocomplete) |
      | `ManyToManyField` | `UnfoldAdminSelectMultipleWidget` |
      | `FileField` | `UnfoldAdminFileFieldWidget` |
      | `ImageField` | `UnfoldAdminImageFieldWidget` |
      
      Only use `formfield_overrides` when you want to **change** from the default (e.g., `BooleanField` to `UnfoldBooleanSwitchWidget`, or `TextField` to `WysiwygWidget`).
      
      ## Widget Override Patterns
      
      ### Method 1: formfield_overrides
      
      Apply widgets to all fields of a type (overrides the defaults above):
      
      ```python
      from django.db import models
      from unfold.widgets import (
          UnfoldAdminTextInputWidget, UnfoldBooleanSwitchWidget,
          UnfoldAdminImageFieldWidget, UnfoldAdminSelect2Widget,
      )
      from unfold.contrib.forms.widgets import WysiwygWidget
      
      class MyAdmin(ModelAdmin):
          formfield_overrides = {
              models.TextField: {"widget": WysiwygWidget},
              models.ImageField: {"widget": UnfoldAdminImageFieldWidget},
              models.BooleanField: {"widget": UnfoldBooleanSwitchWidget},
          }
      ```
      
      ### Method 2: get_form() Override
      
      Apply widgets to specific fields:
      
      ```python
      def get_form(self, request, obj=None, change=False, **kwargs):
          form = super().get_form(request, obj, change, **kwargs)
          form.base_fields["color"].widget = UnfoldAdminColorInputWidget()
          form.base_fields["name"].widget = UnfoldAdminTextInputWidget(attrs={
              "prefix_icon": "search",
              "suffix_icon": "check",
          })
          return form
      ```
      
      ### Method 3: Custom ModelForm
      
      Full control with a custom form class:
      
      ```python
      class MyModelForm(forms.ModelForm):
          flags = forms.MultipleChoiceField(
              choices=[("A", "Option A"), ("B", "Option B")],
              widget=UnfoldAdminCheckboxSelectMultiple,
              required=False,
          )
          custom_select = forms.ChoiceField(
              choices=[("show", "Show"), ("hide", "Hide")],
              widget=UnfoldAdminSelect2Widget,
          )
      
      class MyAdmin(ModelAdmin):
          form = MyModelForm
      ```
      
      ### Text Input with Icons
      
      ```python
      UnfoldAdminTextInputWidget(attrs={
          "prefix_icon": "search",     # Material Symbols icon before input
          "suffix_icon": "euro",       # Material Symbols icon after input
      })
      ```
      
      Or set after init:
      
      ```python
      def __init__(self, *args, **kwargs):
          super().__init__(*args, **kwargs)
          self.fields["name"].widget.attrs.update({
              "prefix_icon": "person",
              "suffix_icon": "verified",
          })
      ```
      
      ## Contrib Widgets
      
      ### WYSIWYG Editor
      
      Requires `unfold.contrib.forms` in `INSTALLED_APPS`:
      
      ```python
      from unfold.contrib.forms.widgets import WysiwygWidget
      
      class MyAdmin(ModelAdmin):
          formfield_overrides = {
              models.TextField: {"widget": WysiwygWidget},
          }
      ```
      
      Uses the Trix editor. The field stores HTML content.
      
      ### Array Widget
      
      For PostgreSQL `ArrayField`:
      
      ```python
      from unfold.contrib.forms.widgets import ArrayWidget
      
      class MyAdmin(ModelAdmin):
          formfield_overrides = {
              ArrayField: {"widget": ArrayWidget},
          }
      
          # With choices (requires get_form override)
          def get_form(self, request, obj=None, change=False, **kwargs):
              form = super().get_form(request, obj, change, **kwargs)
              form.base_fields["tags"].widget = ArrayWidget(choices=TagChoices)
              return form
      ```
      
      ## Inline Classes
      
      ### Imports
      
      ```python
      from unfold.admin import TabularInline, StackedInline, GenericStackedInline
      from unfold.contrib.inlines.admin import NonrelatedStackedInline, NonrelatedTabularInline
      ```
      
      ### Basic Inline
      
      ```python
      class OrderItemInline(TabularInline):
          model = OrderItem
          fields = ["product", "quantity", "price"]
          extra = 1
          show_change_link = True
      ```
      
      ### Inline as Tab
      
      ```python
      class OrderItemInline(TabularInline):
          model = OrderItem
          tab = True  # renders as a tab instead of inline block
      ```
      
      ### Paginated Inline
      
      ```python
      class CommentInline(StackedInline):
          model = Comment
          per_page = 10  # paginate after 10 items
      ```
      
      ### Sortable Inline (Drag-to-Reorder)
      
      ```python
      class MenuItemInline(TabularInline):
          model = MenuItem
          ordering_field = "weight"  # integer field for sort order
          ordering = ["weight"]
      ```
      
      The model needs an integer field (typically `weight` or `order`) that stores position.
      
      ### Collapsible Inline
      
      ```python
      class NoteInline(StackedInline):
          model = Note
          collapsible = True
          classes = ["collapse"]  # Django's built-in collapse also works
      ```
      
      ### Hide Title
      
      ```python
      class StandingInline(StackedInline):
          model = Standing
          hide_title = True
      ```
      
      ### Custom Inline Title
      
      Override the default inline title by adding `get_inline_title()` to the model:
      
      ```python
      # models.py
      class RelatedModel(models.Model):
          name = models.CharField(max_length=100)
      
          def get_inline_title(self):
              return f"Custom: {self.name}"
      ```
      
      ### Combined Example
      
      ```python
      class OrderItemInline(TabularInline):
          model = OrderItem
          tab = True
          per_page = 5
          ordering_field = "weight"
          hide_title = True
          collapsible = True
          autocomplete_fields = ["product"]
          show_change_link = True
          extra = 0
          max_num = 20
      ```
      
      ## Nonrelated Inlines
      
      Display models without a direct ForeignKey relationship:
      
      ```python
      from unfold.contrib.inlines.admin import NonrelatedStackedInline
      
      class RecentOrdersInline(NonrelatedStackedInline):
          model = Order
          fields = ["id", "total", "status"]
          extra = 0
          tab = True
          per_page = 10
      
          def get_form_queryset(self, obj):
              """Return queryset for the inline based on parent obj."""
              return self.model.objects.filter(customer__user=obj).order_by("-created_at")
      
          def save_new_instance(self, parent, instance):
              """Define how to save new instances."""
              instance.customer = parent.customer
      ```
      
      Requires `unfold.contrib.inlines` in `INSTALLED_APPS`.
      
      ### Generic Inlines
      
      For GenericForeignKey relationships:
      
      ```python
      from unfold.admin import GenericStackedInline
      
      class TagInline(GenericStackedInline):
          model = Tag
      ```
      
      ## Inline Configuration
      
      ### Full Attribute Reference
      
      | Attribute | Type | Default | Description |
      |-----------|------|---------|-------------|
      | `tab` | bool | False | Render as tab |
      | `per_page` | int | None | Items per page (pagination) |
      | `ordering_field` | str | None | Field for drag-to-reorder |
      | `hide_title` | bool | False | Hide inline heading |
      | `collapsible` | bool | False | Allow collapse/expand |
      | Standard Django inline attributes also apply |
      
      ## User Forms
      
      Unfold provides styled versions of Django's user admin forms:
      
      ```python
      from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm
      
      @admin.register(User)
      class UserAdmin(BaseUserAdmin, ModelAdmin):
          form = UserChangeForm
          add_form = UserCreationForm
          change_password_form = AdminPasswordChangeForm
      ```
      
      ## Readonly Field Processing
      
      Transform readonly field display using `readonly_preprocess_fields`:
      
      ```python
      class MyAdmin(ModelAdmin):
          readonly_preprocess_fields = {
              "description": "html",      # render as HTML (not escaped)
              "metadata": "json",         # pretty-printed JSON
              "avatar": "image",          # render as image thumbnail
              "document": "file",         # render as file download link
              "config": lambda content: f"<pre>{content}</pre>",  # custom callable
          }
      ```
      
      Built-in preprocessors: `"html"`, `"json"`, `"image"`, `"file"`. Pass a callable for custom transformations.
      
      ## Custom Form Fields
      
      ### Crispy Forms Integration
      
      Unfold supports django-crispy-forms for custom action forms and standalone pages. Create views that extend Unfold's base templates for consistent styling:
      
      ```python
      from django.views.generic import FormView
      
      class CustomFormView(FormView):
          template_name = "myapp/custom_form.html"
          model_admin = None  # set when registering URL
      
          def get_context_data(self, **kwargs):
              context = super().get_context_data(**kwargs)
              context.update(self.model_admin.admin_site.each_context(self.request))
              return context
      ```
      
      Register via `get_urls()`:
      
      ```python
      def get_urls(self):
          return super().get_urls() + [
              path("custom/", self.admin_site.admin_view(
                  CustomFormView.as_view(model_admin=self)
              ), name="custom_form"),
          ]
      ```
      
      ## Custom Pages
      
      Use `UnfoldModelAdminViewMixin` for class-based views that integrate with Unfold's layout:
      
      ```python
      from django.views.generic import TemplateView
      from unfold.views import UnfoldModelAdminViewMixin
      
      class MyCustomPage(UnfoldModelAdminViewMixin, TemplateView):
          title = "Custom Page"
          permission_required = ()  # tuple of required permissions
          template_name = "myapp/custom_page.html"
      ```
      
      Register via `get_urls()` with `model_admin=self`:
      
      ```python
      def get_urls(self):
          view = self.admin_site.admin_view(
              MyCustomPage.as_view(model_admin=self)
          )
          return super().get_urls() + [
              path("custom-page/", view, name="custom_page"),
          ]
      ```
      
      ## Custom Admin Sites
      
      Override the default admin site for full control:
      
      ```python
      from unfold.sites import UnfoldAdminSite
      
      class CustomAdminSite(UnfoldAdminSite):
          pass
      
      custom_site = CustomAdminSite(name="custom_admin")
      ```
      
      Register models with `site=`:
      
      ```python
      @admin.register(MyModel, site=custom_site)
      class MyModelAdmin(ModelAdmin):
          pass
      ```
      
      To override the default admin site globally, use `BasicAppConfig`:
      
      ```python
      # settings.py
      INSTALLED_APPS = [
          "unfold.apps.BasicAppConfig",  # NOT just "unfold"
          "django.contrib.admin",
      ]
      
      # apps.py
      from django.contrib.admin.apps import AdminConfig
      
      class MyAdminConfig(AdminConfig):
          default_site = "myproject.sites.CustomAdminSite"
      ```
      
      ## Key Import Paths
      
      Quick reference for all major Unfold imports:
      
      ```python
      # Core
      from unfold.admin import ModelAdmin, StackedInline, TabularInline, GenericStackedInline
      from unfold.sites import UnfoldAdminSite
      from unfold.views import UnfoldModelAdminViewMixin
      
      # Decorators and enums
      from unfold.decorators import display, action
      from unfold.enums import ActionVariant
      
      # Forms
      from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm
      
      # Dashboard
      from unfold.components import BaseComponent, register_component
      from unfold.datasets import BaseDataset
      from unfold.sections import TableSection, TemplateSection
      from unfold.paginator import InfinitePaginator
      
      # Command palette
      from unfold.dataclasses import SearchResult
      
      # Filters
      from unfold.contrib.filters.admin import (
          TextFilter, DropdownFilter, MultipleDropdownFilter,
          ChoicesDropdownFilter, MultipleChoicesDropdownFilter,
          RelatedDropdownFilter, MultipleRelatedDropdownFilter,
          RelatedCheckboxFilter, ChoicesCheckboxFilter, AllValuesCheckboxFilter,
          BooleanRadioFilter, CheckboxFilter, AutocompleteSelectMultipleFilter,
          RangeNumericFilter, SingleNumericFilter, SliderNumericFilter,
          RangeNumericListFilter, RangeDateFilter, RangeDateTimeFilter,
      )
      
      # Contrib widgets
      from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget
      from unfold.contrib.inlines.admin import NonrelatedStackedInline, NonrelatedTabularInline
      from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm
      ```
      
  • SKILL.md 16.6 KB
    ---
    name: unfold-admin
    description: "Django Unfold admin theme - build, configure, and enhance modern Django admin interfaces with Unfold. Use when working with: (1) Django admin UI customisation or theming, (2) Unfold ModelAdmin, inlines, actions, filters, widgets, or decorators, (3) Admin dashboard components and KPI cards, (4) Sidebar navigation, tabs, or conditional fields, (5) Any mention of 'unfold', 'django-unfold', or 'unfold admin'. Covers the full Unfold feature set: site configuration, actions system, display decorators, filter types, widget overrides, inline variants, dashboard components, datasets, sections, theming, and third-party integrations."
    license: MIT
    metadata:
      author: claude-mods
    ---
    
    # Django Unfold Admin
    
    Modern Django admin theme with Tailwind CSS, HTMX, and Alpine.js. Replaces Django's default admin with a polished, feature-rich interface.
    
    ## Quick Start
    
    ### Installation
    
    ```python
    # settings.py - unfold MUST be before django.contrib.admin
    INSTALLED_APPS = [
        "unfold",
        "unfold.contrib.filters",          # advanced filters
        "unfold.contrib.forms",            # array/wysiwyg widgets
        "unfold.contrib.inlines",          # nonrelated inlines
        "unfold.contrib.import_export",    # styled import/export
        "unfold.contrib.guardian",         # django-guardian integration
        "unfold.contrib.simple_history",   # django-simple-history integration
        "unfold.contrib.constance",        # django-constance integration
        "unfold.contrib.location_field",   # django-location-field integration
        # ...
        "django.contrib.admin",
    ]
    ```
    
    ### Minimal Admin
    
    ```python
    from unfold.admin import ModelAdmin
    
    @admin.register(MyModel)
    class MyModelAdmin(ModelAdmin):
        pass  # inherits Unfold styling
    ```
    
    ### Site Configuration
    
    Replace the default `AdminSite` or configure via `UNFOLD` dict in settings. See [references/configuration.md](references/configuration.md) for the complete settings reference.
    
    ```python
    UNFOLD = {
        "SITE_TITLE": "My Admin",
        "SITE_HEADER": "My Admin",
        "SITE_SYMBOL": "dashboard",  # Material Symbols icon name
        "SIDEBAR": {
            "show_search": True,
            "navigation": [
                {
                    "title": _("Navigation"),
                    "items": [
                        {
                            "title": _("Dashboard"),
                            "icon": "dashboard",
                            "link": reverse_lazy("admin:index"),
                        },
                    ],
                },
            ],
        },
    }
    ```
    
    ## Core Workflow
    
    When building Unfold admin interfaces, follow this sequence:
    
    1. **Configure site** - UNFOLD settings dict (branding, sidebar, theme)
    2. **Register models** - Extend `unfold.admin.ModelAdmin`
    3. **Enhance display** - `@display` decorator for list columns
    4. **Add actions** - `@action` decorator for row/list/detail/submit actions
    5. **Configure filters** - Replace default filters with Unfold filter classes
    6. **Override widgets** - Apply Unfold widgets via `formfield_overrides`
    7. **Set up inlines** - Use Unfold's inline classes with tabs, pagination, sorting
    8. **Build dashboard** - `@register_component` + `BaseComponent` for KPI cards
    
    ## ModelAdmin Attributes
    
    Unfold extends Django's `ModelAdmin` with these additional attributes:
    
    | Attribute | Type | Purpose |
    |-----------|------|---------|
    | `list_fullwidth` | bool | Full-width changelist (no sidebar) |
    | `list_filter_submit` | bool | Add submit button to filters |
    | `list_filter_sheet` | bool | Filters in sliding sheet panel |
    | `compressed_fields` | bool | Compact field spacing in forms |
    | `warn_unsaved_form` | bool | Warn before leaving unsaved form |
    | `ordering_field` | str | Field name for drag-to-reorder |
    | `hide_ordering_field` | bool | Hide the ordering field column |
    | `list_horizontal_scrollbar_top` | bool | Scrollbar at top of list |
    | `list_disable_select_all` | bool | Disable "select all" checkbox |
    | `change_form_show_cancel_button` | bool | Show cancel button on form |
    | `actions_list` | list | Global changelist actions |
    | `actions_row` | list | Per-row actions in changelist |
    | `actions_detail` | list | Actions on change form |
    | `actions_submit_line` | list | Actions in form submit area |
    | `actions_list_hide_default` | bool | Hide default list actions |
    | `actions_detail_hide_default` | bool | Hide default detail actions |
    | `conditional_fields` | dict | JS expressions for field visibility |
    | `change_form_datasets` | list | BaseDataset subclasses for change form |
    | `list_sections` | list | TableSection/TemplateSection for list |
    | `list_sections_classes` | str | CSS grid classes for sections |
    | `readonly_preprocess_fields` | dict | Transform readonly field content |
    | `add_fieldsets` | list | Separate fieldsets for add form (like UserAdmin) |
    
    ### Template Injection Points
    
    Insert custom HTML before/after changelist or change form:
    
    ```python
    class MyAdmin(ModelAdmin):
        # Changelist
        list_before_template = "myapp/list_before.html"
        list_after_template = "myapp/list_after.html"
        # Change form (inside <form> tag)
        change_form_before_template = "myapp/form_before.html"
        change_form_after_template = "myapp/form_after.html"
        # Change form (outside <form> tag)
        change_form_outer_before_template = "myapp/outer_before.html"
        change_form_outer_after_template = "myapp/outer_after.html"
    ```
    
    ### Conditional Fields
    
    Show/hide fields based on other field values (Alpine.js expressions):
    
    ```python
    class MyAdmin(ModelAdmin):
        conditional_fields = {
            "premium_features": "plan == 'PRO'",
            "discount_amount": "has_discount == true",
        }
    ```
    
    ## Actions System
    
    Four action types, each with different signatures. See [references/actions-filters.md](references/actions-filters.md) for complete reference.
    
    ```python
    from unfold.decorators import action
    from unfold.enums import ActionVariant
    
    # List action (no object context)
    @action(description=_("Rebuild Index"), icon="sync", variant=ActionVariant.PRIMARY)
    def rebuild_index(self, request):
        # process...
        return redirect(request.headers["referer"])
    
    # Row action (receives object_id)
    @action(description=_("Approve"), url_path="approve")
    def approve_row(self, request, object_id):
        obj = self.model.objects.get(pk=object_id)
        return redirect(request.headers["referer"])
    
    # Detail action (receives object_id, shown on change form)
    @action(description=_("Send Email"), permissions=["send_email"])
    def send_email(self, request, object_id):
        return redirect(reverse_lazy("admin:myapp_mymodel_change", args=[object_id]))
    
    # Submit line action (receives obj instance, runs on save)
    @action(description=_("Save & Publish"))
    def save_and_publish(self, request, obj):
        obj.published = True
    ```
    
    ### Action Groups (Dropdown Menus)
    
    ```python
    actions_list = [
        "primary_action",
        {
            "title": _("More"),
            "variant": ActionVariant.PRIMARY,
            "items": ["secondary_action", "tertiary_action"],
        },
    ]
    ```
    
    ### Permissions
    
    ```python
    @action(permissions=["can_export", "auth.view_user"])
    def export_data(self, request):
        pass
    
    def has_can_export_permission(self, request):
        return request.user.is_superuser
    ```
    
    ## Display Decorator
    
    Enhance list_display columns. See [references/actions-filters.md](references/actions-filters.md).
    
    ```python
    from unfold.decorators import display
    
    # Colored status labels
    @display(description=_("Status"), ordering="status", label={
        "active": "success",    # green
        "pending": "info",      # blue
        "warning": "warning",   # orange
        "inactive": "danger",   # red
    })
    def show_status(self, obj):
        return obj.status
    
    # Rich header with avatar
    @display(description=_("User"), header=True)
    def show_header(self, obj):
        return [
            obj.full_name,           # primary text
            obj.email,               # secondary text
            obj.initials,            # badge text
            {"path": obj.avatar.url, "width": 24, "height": 24, "borderless": True},
        ]
    
    # Interactive dropdown
    @display(description=_("Teams"), dropdown=True)
    def show_teams(self, obj):
        return {
            "title": f"{obj.teams.count()} teams",
            "items": [{"title": t.name, "link": t.get_admin_url()} for t in obj.teams.all()],
            "striped": True,
            "max_height": 200,
        }
    
    # Boolean checkmark
    @display(description=_("Active"), boolean=True)
    def is_active(self, obj):
        return obj.is_active
    ```
    
    ## Filters
    
    Unfold provides advanced filter classes. See [references/actions-filters.md](references/actions-filters.md).
    
    ```python
    from unfold.contrib.filters.admin import (
        TextFilter, RangeNumericFilter, RangeDateFilter, RangeDateTimeFilter,
        SingleNumericFilter, SliderNumericFilter, RelatedDropdownFilter,
        RelatedCheckboxFilter, ChoicesCheckboxFilter, AllValuesCheckboxFilter,
        BooleanRadioFilter, CheckboxFilter, AutocompleteSelectMultipleFilter,
    )
    
    class MyAdmin(ModelAdmin):
        list_filter_submit = True  # required for input-based filters
        list_filter = [
            ("salary", RangeNumericFilter),
            ("status", ChoicesCheckboxFilter),
            ("created_at", RangeDateFilter),
            ("category", RelatedDropdownFilter),
            ("is_active", BooleanRadioFilter),
        ]
    ```
    
    ### Custom Text Filter
    
    ```python
    class NameFilter(TextFilter):
        title = _("Name")
        parameter_name = "name"
    
        def queryset(self, request, queryset):
            if self.value() in EMPTY_VALUES:
                return queryset
            return queryset.filter(name__icontains=self.value())
    ```
    
    ## Widgets
    
    Override form widgets for Unfold styling. See [references/widgets-inlines.md](references/widgets-inlines.md).
    
    ```python
    from unfold.widgets import (
        UnfoldAdminTextInputWidget, UnfoldAdminSelectWidget, UnfoldAdminSelect2Widget,
        UnfoldBooleanSwitchWidget, UnfoldAdminColorInputWidget,
        UnfoldAdminSplitDateTimeWidget, UnfoldAdminImageFieldWidget,
    )
    from unfold.contrib.forms.widgets import WysiwygWidget, ArrayWidget
    
    class MyAdmin(ModelAdmin):
        formfield_overrides = {
            models.TextField: {"widget": WysiwygWidget},
            models.ImageField: {"widget": UnfoldAdminImageFieldWidget},
        }
    ```
    
    ### Text Input with Icons
    
    ```python
    widget = UnfoldAdminTextInputWidget(attrs={
        "prefix_icon": "search",
        "suffix_icon": "euro",
    })
    ```
    
    ## Inlines
    
    Unfold inlines support tabs, pagination, sorting, and nonrelated models. See [references/widgets-inlines.md](references/widgets-inlines.md).
    
    ```python
    from unfold.admin import TabularInline, StackedInline
    from unfold.contrib.inlines.admin import NonrelatedStackedInline
    
    class OrderItemInline(TabularInline):
        model = OrderItem
        tab = True          # show as tab
        per_page = 10       # paginated
        ordering_field = "weight"  # drag-to-reorder
        hide_title = True
        collapsible = True
    ```
    
    ## Fieldset Tabs
    
    Group fieldsets into tabs using `"classes": ["tab"]`:
    
    ```python
    fieldsets = [
        (None, {"fields": ["name", "email"]}),  # always visible
        (_("Profile"), {"classes": ["tab"], "fields": ["bio", "avatar"]}),
        (_("Settings"), {"classes": ["tab"], "fields": ["theme", "notifications"]}),
    ]
    ```
    
    ## Dashboard Components
    
    Build KPI cards and custom dashboard widgets. See [references/dashboard.md](references/dashboard.md).
    
    ```python
    from unfold.components import BaseComponent, register_component
    from django.template.loader import render_to_string
    
    @register_component
    class ActiveUsersComponent(BaseComponent):
        def get_context_data(self, **kwargs):
            context = super().get_context_data(**kwargs)
            context["children"] = render_to_string("myapp/kpi_card.html", {
                "total": User.objects.filter(is_active=True).count(),
                "label": "Active Users",
            })
            return context
    ```
    
    Configure in settings:
    
    ```python
    UNFOLD = {
        "DASHBOARD_CALLBACK": "myapp.views.dashboard_callback",
    }
    ```
    
    ## Sections (Changelist Panels)
    
    Embed related data panels in changelist views:
    
    ```python
    from unfold.sections import TableSection, TemplateSection
    
    class RecentOrdersSection(TableSection):
        related_name = "order_set"
        fields = ["id", "total", "status"]
        height = 380
    
    class ChartSection(TemplateSection):
        template_name = "myapp/chart.html"
    
    class MyAdmin(ModelAdmin):
        list_sections = [RecentOrdersSection, ChartSection]
        list_sections_classes = "lg:grid-cols-2"
    ```
    
    ## Datasets (Change Form Panels)
    
    Embed model listings within change forms:
    
    ```python
    from unfold.datasets import BaseDataset
    
    class RelatedItemsDatasetAdmin(ModelAdmin):
        list_display = ["name", "status"]
        search_fields = ["name"]
    
    class RelatedItemsDataset(BaseDataset):
        model = RelatedItem
        model_admin = RelatedItemsDatasetAdmin
        tab = True  # show as tab
    
    class MyAdmin(ModelAdmin):
        change_form_datasets = [RelatedItemsDataset]
    ```
    
    ## Paginator
    
    Use infinite scroll pagination:
    
    ```python
    from unfold.paginator import InfinitePaginator
    
    class MyAdmin(ModelAdmin):
        paginator = InfinitePaginator
        show_full_result_count = False
        list_per_page = 20
    ```
    
    ## Third-Party Integrations
    
    Unfold provides styled wrappers for common Django packages. See [references/resources.md](references/resources.md) for complete setup guides.
    
    | Package | Unfold Module | Setup |
    |---------|--------------|-------|
    | django-import-export | `unfold.contrib.import_export` | Use `ImportForm`, `ExportForm`, `SelectableFieldsExportForm` |
    | django-guardian | `unfold.contrib.guardian` | Styled guardian integration |
    | django-simple-history | `unfold.contrib.simple_history` | Styled history integration |
    | django-constance | `unfold.contrib.constance` | Styled constance config |
    | django-location-field | `unfold.contrib.location_field` | Location widget |
    | django-modeltranslation | Compatible | Mix `TabbedTranslationAdmin` with `ModelAdmin` |
    | django-celery-beat | Compatible (rewire) | Unregister 5 models, re-register with Unfold |
    | django-money | `unfold.widgets` | `UnfoldAdminMoneyWidget` |
    | djangoql | Compatible | Mix `DjangoQLSearchMixin` with `ModelAdmin` |
    | django-crispy-forms | Compatible | Unfold template pack available |
    
    ```python
    # Multiple inheritance - Unfold ModelAdmin always last
    @admin.register(MyModel)
    class MyAdmin(DjangoQLSearchMixin, SimpleHistoryAdmin, GuardedModelAdmin, ModelAdmin):
        pass
    ```
    
    ## Built-In Template Components
    
    Unfold ships reusable template components for dashboards and custom pages:
    
    | Component | Path | Key Variables |
    |-----------|------|---------------|
    | Card | `unfold/components/card.html` | `title`, `footer`, `label`, `icon` |
    | Bar Chart | `unfold/components/chart/bar.html` | `data` (JSON), `height`, `width` |
    | Line Chart | `unfold/components/chart/line.html` | `data` (JSON), `height`, `width` |
    | Progress | `unfold/components/progress.html` | `value`, `title`, `description` |
    | Table | `unfold/components/table.html` | `table`, `card_included`, `striped` |
    | Button | `unfold/components/button.html` | `name`, `href`, `submit` |
    | Tracker | `unfold/components/tracker.html` | `data` |
    | Cohort | `unfold/components/cohort.html` | `data` |
    
    ```html
    {% load unfold %}
    {% component "MyKPIComponent" %}{% endcomponent %}
    ```
    
    ## User Admin Forms
    
    Unfold provides styled versions of Django's auth admin forms:
    
    ```python
    from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm
    
    @admin.register(User)
    class UserAdmin(BaseUserAdmin, ModelAdmin):
        form = UserChangeForm
        add_form = UserCreationForm
        change_password_form = AdminPasswordChangeForm
    ```
    
    ## Reference Files
    
    Detailed documentation split by topic:
    
    - **[references/configuration.md](references/configuration.md)** - Complete UNFOLD settings dict, sidebar, tabs, theming, environment, login
    - **[references/actions-filters.md](references/actions-filters.md)** - Action types and signatures, display decorator, all filter classes
    - **[references/widgets-inlines.md](references/widgets-inlines.md)** - Complete widget class list (35+), inline variants, nonrelated inlines, forms
    - **[references/dashboard.md](references/dashboard.md)** - Dashboard components, sections, datasets, custom templates, Tailwind patterns
    - **[references/resources.md](references/resources.md)** - Official links, all third-party integrations, common patterns, version compatibility, Unfold Studio
    
    Read the relevant reference file when you need detailed configuration options, the full list of available classes, complete code examples, or integration setup guides for a specific feature area.
    
    ### Key External References
    
    | Resource | URL |
    |----------|-----|
    | Docs | https://unfoldadmin.com/docs/ |
    | GitHub | https://github.com/unfoldadmin/django-unfold |
    | Demo App (Formula) | https://github.com/unfoldadmin/formula |
    | Live Demo | https://demo.unfoldadmin.com |
    | Material Symbols (Icons) | https://fonts.google.com/icons |
    
    When uncertain about an implementation pattern, consult `formula/admin.py` and `formula/settings.py` in the Formula demo repo - it covers virtually every Unfold feature.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related