# 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" %}
$42,000
{% 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