django-unfold ยท diff
v1.1.0 to v2.0.0
9 added, 578 removed. Audit A to A.
---
name: django-unfold
- description: "Modern Django admin theme - Unfold - customization, settings, components, actions, filters, integrations"
+ description: Use when theming the Django admin with django-unfold - settings and sidebar configuration, custom admin site, components and @display decorator, filters, actions, tabs, dark mode and styling, or Django 6.x compatibility
metadata:
author: mte90
- version: 1.1.0
+ version: 2.0.0
based_on: https://github.com/unfoldadmin/django-unfold
tags:
- python
- django
- admin
- unfold
- theme
- dashboard
---
# Django Unfold
Modern Django admin theme with beautiful design and advanced features.
## Overview
Unfold is a modern theme for Django admin that provides:
**Versions**: django-unfold 0.76.x + Django 6.0 fully compatible.
- **Beautiful design** - Modern UI with Tailwind CSS
- **Dark mode** - Built-in dark theme support
- **Custom components** - Charts, tables, cards, buttons
- **Easy customization** - Settings, branding, sidebar
- **Integrations** - Works with django-celery-beat, django-import-export, etc.
---
## Installation
```bash
pip install django-unfold
# Or with specific version
pip install django-unfold==0.9.0
```
```python
# settings.py
INSTALLED_APPS = [
'unfold', # Must come BEFORE django.contrib.admin
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
# ... other apps
]
# Unfold settings
UNFOLD = {
# Configuration here
}
```
> **Important**: `unfold` must be first in `INSTALLED_APPS` to override Django templates.
---
## Quick Start
### Basic Configuration
```python
# settings.py
INSTALLED_APPS = [
'unfold',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
]
# Optional: Add your custom admin site class
# UNFOLD_ADMIN_SITE_CLASS = 'path.to.CustomAdminSite'
```
### URL Configuration
Unfold doesn't require changes to your URL configuration:
```python
# urls.py - No changes needed!
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
# ... other urls
]
```
---
- ## Settings Reference
-
- ### Site Branding
-
- ```python
- UNFOLD = {
- 'SITE_HEADER': 'My Company Admin',
- 'SITE_TITLE': 'My Admin',
- 'INDEX_TITLE': 'Welcome to Dashboard',
-
- # Or with HTML support
- 'SITE_HEADER': '<div class="flex items-center gap-2"><span>๐</span> My Company</div>',
- }
- ```
-
- ### Colors and Theme
-
- ```python
- UNFOLD = {
- 'COLORS': {
- 'primary': {
- '50': '239 246 255',
- '100': '219 234 254',
- '200': '191 219 254',
- '300': '147 197 253',
- '400': '96 165 250',
- '500': '59 130 246',
- '600': '37 99 235',
- '700': '29 78 216',
- '800': '30 64 175',
- '900': '30 58 138',
- '950': '30 58 138',
- },
- },
-
- 'DARK_MODE_COLORS': {
- 'primary': {
- '50': '239 246 255',
- # ... dark mode colors
- },
- },
- }
- ```
-
- ### Sidebar Navigation
-
- ```python
- UNFOLD = {
- 'SIDEBAR': {
- 'show_search': True,
- 'navigation': [
- {
- 'title': 'Main',
- 'items': [
- {'title': 'Dashboard', 'icon': 'dashboard', 'link': '/admin/'},
- {'title': 'Users', 'icon': 'people', 'link': '/admin/auth/user/'},
- ],
- },
- {
- 'title': 'Content',
- 'items': [
- {'title': 'Articles', 'icon': 'article', 'link': '/admin/myapp/article/'},
- ],
- },
- ],
- },
- }
- ```
-
- ### Dashboard Widgets
-
- ```python
- UNFOLD = {
- 'DASHBOARD_WIDGETS': [
- 'unfold.widgets.DashboardStatistics',
- 'unfold.widgets.DashboardActions',
- # Custom widgets
- ],
- }
- ```
-
- ### Feature Flags
-
- ```python
- UNFOLD = {
- # Show/hide features
- 'SHOW_HISTORY': True,
- 'SHOW_VIEW_ON_SITE': True,
-
- # Disable specific features
- 'AUTH_PASSWORD_VALIDATION': True,
- }
- ```
-
- ---
-
- ## Custom Admin Site
-
- ### Custom Site Class
-
- ```python
- # admin.py
- from unfold.admin import ModelAdmin, UnfoldAdminSite
- from django.contrib.admin import AdminSite
-
- class CustomAdminSite(UnfoldAdminSite):
- site_header = 'My Custom Admin'
- site_title = 'My Admin Panel'
- index_title = 'Welcome to Management'
-
- def each_context(self, request):
- context = super().each_context(request)
- # Add custom context
- context['custom_data'] = 'value'
- return context
-
- admin_site = CustomAdminSite(name='myadmin')
- ```
-
- ```python
- # settings.py
- UNFOLD_ADMIN_SITE_CLASS = 'myapp.admin.CustomAdminSite'
- ```
-
- ### Custom ModelAdmin
-
- ```python
- # admin.py
- from django.contrib import admin
- from unfold.admin import ModelAdmin
- from .models import Article
-
- @admin.register(Article, site=custom_admin_site)
- class ArticleAdmin(ModelAdmin):
- list_display = ['title', 'status', 'created_at']
- search_fields = ['title', 'content']
- list_filter = ['status', 'created_at']
-
- # Unfold-specific
- sidebar_fieldsets = (
- (None, {'fields': ('title', 'slug')}),
- ('Content', {'fields': ('content', 'excerpt')}),
- )
- ```
-
- ---
-
- ## Components
-
- ### Buttons
-
- ```python
- from unfold.decorators import action
- from unfold.actions import Actions
-
- class ArticleAdmin(ModelAdmin):
- @action(description='Publish selected')
- def make_published(self, request, queryset):
- queryset.update(status='published')
-
- @action(description='Export to CSV')
- def export_csv(self, request, queryset):
- # Export logic
- pass
- ```
-
- ### @display Decorator (Django 6.0+)
-
- ```python
- from unfold.decorators import display
-
- class UserAdmin(ModelAdmin):
- @display(description="Status", boolean=True)
- def is_active(self, obj):
- return obj.is_active
-
- @display(description="Actions", order="username")
- def user_actions(self, obj):
- return f"{obj.first_name} {obj.last_name}"
- ```
-
- ### Cards
-
- ```python
- # In change_view.html or custom template
- {% load unfold %}
-
- {% component "card" title="Statistics" %}
- <div class="p-4">
- <p class="text-2xl font-bold">1,234</p>
- <p class="text-gray-500">Total Users</p>
- </div>
- {% endcomponent %}
- ```
-
- ### Charts
-
- ```python
- # Using Unfold chart component
- {% component "chart" type="line" data=chart_data %}
- {% endcomponent %}
- ```
-
- ### Tables
-
- ```python
- # In list_display
- class UserAdmin(ModelAdmin):
- list_display = ['username', 'email', 'status_badge']
-
- @bind_to(admin_order_field='is_active')
- def status_badge(self, obj):
- from unfold.helpers import icon
- if obj.is_active:
- return icon('check_circle', classes='text-green-500')
- return icon('x_circle', classes='text-red-500')
- ```
-
- ---
-
- ## Fields and Widgets
-
- ### Autocomplete Fields
-
- ```python
- from unfold import fields
- from unfold.forms import ModelForm
-
- class ArticleForm(ModelForm):
- class Meta:
- model = Article
- fields = '__all__'
-
- author = fields.AutocompleteField(
- queryset=User.objects.all(),
- search_fields=['username', 'email'],
- label='Author'
- )
- ```
-
- ### JSON Fields
-
- ```python
- from unfold.fields import JSONField
-
- class ConfigAdmin(ModelAdmin):
- fieldsets = (
- (None, {
- 'fields': ('config_json',)
- }),
- )
-
- def get_form(self, request, obj=None, **kwargs):
- form = super().get_form(request, obj, **kwargs)
- form.base_fields['config_json'] = fields.JSONField(
- widget=forms.Textarea(attrs={
- 'class': 'font-mono text-sm',
- 'rows': 10
- })
- )
- return form
- ```
-
- ---
-
- ## Filters
-
- ### Custom Filters
-
- ```python
- import unfold.filters as filters
-
- class ArticleAdmin(ModelAdmin):
- list_filter = [
- ('status', filters.DropdownFilter),
- ('category', filters.DropdownFilter),
- ('created_at', filters.DateRangeFilter),
- ('author', filters.AutocompleteFilter),
- ]
- ```
-
- ### Filter Types
-
- ```python
- # Dropdown filter
- ('status', filters.DropdownFilter)
-
- # Date range filter
- ('created_at', filters.DateRangeFilter)
-
- # Autocomplete filter (for FK/M2M with many items)
- ('author', filters.AutocompleteFilter)
-
- # Checkbox/radio filter
- ('is_published', filters.CheckboxFilter)
-
- # Numeric range filter
- ('views', filters.NumericFilter)
-
- # Text search filter
- ('title', filters.TextFilter)
- ```
-
- ---
-
- ## Actions
-
- ### Custom Actions
-
- ```python
- from django.http import HttpResponse
- from django.shortcuts import render
- import csv
- import io
-
- class ArticleAdmin(ModelAdmin):
- @action(description='Export selected to CSV')
- def export_csv(self, request, queryset):
- # Create CSV
- buffer = io.StringIO()
- writer = csv.writer(buffer)
- writer.writerow(['Title', 'Status', 'Created'])
-
- for obj in queryset:
- writer.writerow([obj.title, obj.status, obj.created])
-
- # Return response
- response = HttpResponse(buffer.getvalue(), content_type='text/csv')
- response['Content-Disposition'] = 'attachment; filename="articles.csv"'
- return response
-
- @action(description='Send to publication')
- def publish(self, request, queryset):
- queryset.update(status='published', published_at=timezone.now())
- publish.short_description = 'Publish selected articles'
- ```
-
- ### Row Actions
-
- ```python
- class ArticleAdmin(ModelAdmin):
- def get_row_actions(self, obj):
- actions = super().get_row_actions(obj)
- actions.append(
- actions.Link(
- 'preview',
- icon='visibility',
- link=f'/admin/myapp/article/{obj.pk}/preview/'
- )
- )
- return actions
- ```
-
- ---
-
- ## Tabs
-
- ### Using Tabs
-
- ```python
- class ArticleAdmin(ModelAdmin):
- tabs = [
- {'title': 'Content', 'id': 'content'},
- {'title': 'SEO', 'id': 'seo'},
- {'title': 'Metadata', 'id': 'metadata'},
- ]
-
- fieldsets = (
- (None, {
- 'fields': ('title', 'content'),
- 'tab_id': 'content',
- }),
- ('SEO Settings', {
- 'fields': ('meta_title', 'meta_description'),
- 'tab_id': 'seo',
- }),
- ('Metadata', {
- 'fields': ('created_at', 'updated_at'),
- 'tab_id': 'metadata',
- }),
- )
- ```
-
- ---
-
## Integrations
### django-guardian (Object Permissions)
```python
# Install
pip install django-guardian
# Add to INSTALLED_APPS
INSTALLED_APPS = [
'guardian',
'unfold',
'django.contrib.admin',
# ...
]
# Add to URLs
from django.contrib import admin
from unfold.contrib.guardian.admin import GuardedModelAdmin
class ArticleAdmin(GuardedModelAdmin):
pass
```
### django-import-export
```python
pip install django-import-export
# Works automatically with Unfold
# Custom resource if needed
from import_export import resources
from unfold.admin import ImportExportMixin
class ArticleResource(resources.ModelResource):
class Meta:
model = Article
fields = ('id', 'title', 'status', 'created_at')
class ArticleAdmin(ImportExportMixin, ModelAdmin):
resource_classes = [ArticleResource]
```
### django-celery-beat
```python
# Install
pip install django-celery-beat
# Configuration is automatic with Unfold
# Shows schedule in admin dashboard
```
### django-constance
```python
pip install django-constance[database]
INSTALLED_APPS = [
'constance',
'unfold',
# ...
]
# Configuration is automatic
# Shows in admin under "Constance" section
```
---
## Authentication Customization
### Custom Login Form
```python
# Custom form for Unfold login
from django import forms
from unfold.forms import LoginForm
class CustomLoginForm(LoginForm):
def clean(self):
# Add custom validation
cleaned_data = super().clean()
# Custom logic
return cleaned_data
```
```python
# settings.py
UNFOLD = {
'LOGIN': {
'form': 'myapp.forms.CustomLoginForm',
},
}
```
### Custom Views
```python
# Custom password reset
UNFOLD = {
'PASSWORD_CHANGE_FORM': 'myapp.forms.CustomPasswordChangeForm',
'PASSWORD_RESET_FORM': 'myapp.forms.CustomPasswordResetForm',
}
```
---
- ## Dark Mode
-
- ### Automatic Dark Mode
-
- Unfold automatically supports dark mode based on system preference.
-
- ### Manual Control
-
- ```python
- # Force dark mode
- UNFOLD = {
- 'THEME': 'dark', # 'dark' or 'light' or None (auto)
- }
- ```
-
- ### Custom Colors for Dark Mode
-
- ```python
- UNFOLD = {
- 'DARK_MODE_COLORS': {
- 'primary': {
- '50': '224 242 254',
- '100': '214 238 253',
- # ... custom dark mode colors
- },
- },
- }
- ```
-
- ---
-
- ## Custom Styling
-
- ### Custom CSS
-
- ```python
- # settings.py
- UNFOLD = {
- 'STYLES': [
- 'css/custom.css',
- ],
- }
-
- # templates/admin/base.html
- {% extends "admin/base.html" %}
- {% load static %}
-
- {% block extrastyle %}
- <link rel="stylesheet" href="{% static 'css/custom.css' %}">
- {% endblock %}
- ```
-
- ### Custom JavaScript
-
- ```python
- UNFOLD = {
- 'SCRIPTS': [
- 'js/custom.js',
- ],
- }
- ```
-
- ---
-
## Best Practices
1. **Keep unfold first** in INSTALLED_APPS
2. **Use Unfold ModelAdmin** for all models
3. **Leverage tabs** for organized forms
4. **Use filters** for better list navigation
5. **Customize actions** for bulk operations
6. **Enable dark mode** - users love it
7. **Use integrations** - they work out of the box
- ## Ecosystem Libraries
- ### django-unfold-modal
- - **URL**: https://github.com/metaforx/django-unfold-modal
- - **PyPI**: `django-unfold-modal`
- - **Version**: 0.2.1 (requires Python 3.10+, Django 5.0+, django-unfold 0.52.0+)
-
- Replaces Django admin's popup windows for related objects (ForeignKey, ManyToMany, etc.) with Unfold-styled modals, so related-object selection happens in-page instead of in separate browser windows.
-
- **Key features:**
- - Modal replacement for admin related-object popups (ForeignKey/ManyToMany/OneToOne selects, `raw_id_fields` lookup, `autocomplete_fields`/Select2, related fields inside inlines)
- - Nested modals with replace/restore behavior
- - Size presets (`default`/`large`/`full`) and optional manual resize handle
- - Optional admin header suppression inside modal iframes
- - Django CMS integration โ admin modals can open in the CMS parent window when admin runs inside a CMS modal
-
- ```bash
- pip install django-unfold-modal
- ```
-
- ### django-unfold-markdown
- - **URL**: https://github.com/sergei-vasilev-dev/django-unfold-markdown
- - **PyPI**: `django-unfold-markdown`
- - **Version**: 0.1.2 (requires Python >=3.10, Django >=4.2, django-unfold >=0.70.0)
-
- Markdown editor widget for the Django Unfold admin: a plain-text monospace editor with live preview that renders stored content as Markdown.
-
- **Key features:**
- - Plain text editor with monospace font (no rich-text WYSIWYG)
- - Side-by-side live preview and fullscreen mode
- - Dark/light theme integration with Unfold; Material Symbols icons matching Unfold design
- - Toolbar: bold, italic, strikethrough, headings, lists, links, images, tables, horizontal rule
- - No autosave โ content saves on form submit
- - Responsive layout; renders stored text with `markdown` or `mistune`
-
- ```bash
- pip install django-unfold-markdown
- ```
-
- ### django-unfold-extra
- - **URL**: https://github.com/metaforx/django-unfold-extra
- - **PyPI**: `django-unfold-extra`
- - **Version**: 0.4.0 (requires `django-unfold>=0.92`, `django-cms>=5.0,<5.1`, `django-parler~=2.3`)
-
- Unofficial "extra" package that enhances the Django Unfold admin interface with Unfold-styled integrations for Django CMS 5.0, django-parler multilingual models, and django-versatileimagefield.
-
- **Key features:**
- - Django CMS 5.0 integration: Unfold-styled page tree with "New Page" button and language switcher, tabbed Page/PageContent change forms, `PageUser`/`PageUserGroup`/`GlobalPagePermission` admin with permission inlines, `djangocms-versioning` admin
- - `UnfoldCMSPluginBase` with `UnfoldStackedInline`/`UnfoldTabularInline`; drop-in Unfold-styled `djangocms-link` Link plugin and `djangocms-alias` admin
- - django-parler support: `UnfoldTranslatableAdminMixin`, `UnfoldTranslatableStackedAdminMixin`, `UnfoldTranslatableTabularAdminMixin`, translatable inlines
- - django-versatileimagefield integration (preview + ppoi)
- - Unfold auto-update of styles from the official package via npm; Theme-Sync to control themes from either the Unfold or CMS switcher
-
- ```bash
- pip install django-unfold-extra
- ```
-
- > **Note:** Used in production but the author notes to "expect additional implementation work"; recommended mainly when most CMS plugins are custom-built.
-
- ### django-admin-action-forms
- - **URL**: https://github.com/michalpokusa/django-admin-action-forms
- - **PyPI**: `django-admin-action-forms`
- - **Version**: 3.0.0 (supports Django 3.2.x through 6.x.x; requires only `django>=3.2`)
-
- Django admin extension that adds confirmation pages for admin actions: each action can have a custom form shown on an intermediate page before execution, with the form data passed to the action as an extra argument. Works with the stock Django admin (not Unfold-specific, but pairs well with Unfold's admin theming).
-
- **Key features:**
- - `@action_with_form` decorator + `ActionForm` base class; form data passed to the action as an additional argument
- - Supports Django 3.2.x through 6.x.x; no additional dependencies
- - Supports `fields`/`fieldsets`, `filter_horizontal`/`filter_vertical`, `autocomplete_fields`, custom widgets and validators
- - Formset support via inlines (`StackedAdminActionInline`, `TabularAdminActionInline`)
- - Easily testable with Django's built-in testing tools; compatible with `django-no-queryset-admin-actions`
-
- ```bash
- pip install django-admin-action-forms
- ```
- Add `'django_admin_action_forms'` to `INSTALLED_APPS`.
-
- ## Django 6.x `get_action_choices` Compatibility
-
- `django-unfold` 0.105.0's `ModelAdmin.get_action_choices(self, request, default_choices=BLANK_CHOICE_DASH)` does not accept the `action_location` argument that Django 6.x requires, triggering a `DeprecationWarning` that fails the test suite when warnings are treated as errors.
-
- Since this is an upstream bug, the fix is a monkey-patch in an `AppConfig.ready()` hook:
-
- ```python
- # customers/apps.py
- from django.apps import AppConfig
-
- class CustomersConfig(AppConfig):
- name = "customers"
-
- def ready(self):
- from unfold.admin import ModelAdmin
- original = ModelAdmin.get_action_choices
-
- def patched(self, request, default_choices=None, action_location=None):
- return original(self, request, default_choices)
-
- ModelAdmin.get_action_choices = patched
- ```
-
- This runs once at Django startup and silences the warning for the entire process. Remove the patch when `django-unfold` ships a version that accepts `action_location` natively.
-
- ## INSTALLED_APPS Ordering
-
- `django-unfold` must appear in `INSTALLED_APPS` **before** `django.contrib.admin` so its templates and static files take precedence:
-
- ```python
- INSTALLED_APPS = [
- # ...
- "unfold", # before admin
- "django.contrib.admin",
- # ...
- ]
- ```
-
- If the order is reversed, the admin renders with the default Django admin theme and Unfold's customizations are silently ignored.
-
- ## Admin URL Precedence and Action Payload
-
- These patterns apply to Unfold and standard Django admin alike, but surface frequently when customizing Unfold views:
+ ---
- 1. **Custom URLs before `super().get_urls()`** โ A custom Unfold view like a voucher-creation wizard must be registered before the default changelist pattern, or `<path:object_id>/` swallows it.
+ ## Deep Dives
- 2. **Action POST payload** โ when triggering a custom action from an Unfold view via JavaScript, the payload must include `action` (the method name) and `_selected_action` (list of IDs). Swapping them silently no-ops the action.
+ For detailed reference, load these on-demand:
- 3. **Redirects** โ `UnfoldAdminSite` has no `urls_url` attribute. Use `django.urls.reverse()` to build redirect targets from custom Unfold views.
+ - **Settings & Customization** โ `references/settings-customization.md` - Settings reference, custom admin site, dark mode, custom styling
+ - **Components & Actions** โ `references/components-actions.md` - Components, fields, filters, actions, tabs
+ - **Ecosystem & Compatibility** โ `references/ecosystem-compat.md` - Ecosystem libraries, Django 6.x compatibility, ordering notes
- ```python
- from django.urls import reverse
- return redirect(reverse("admin:customers_customer_changelist"))
- ```
+ ---
## References
- **GitHub**: https://github.com/unfoldadmin/django-unfold
- **Documentation**: https://unfoldadmin.com/docs
- **Demo**: https://unfoldadmin.com/