resources.md 17 KB

Resources, Integrations, and Patterns

Table of Contents

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 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

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:

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

# 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

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:

# 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

{% 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)

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:

# 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

# 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:

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:

if "django_celery_beat" in settings.INSTALLED_APPS:
    @admin.register(PeriodicTask)
    class PeriodicTaskAdmin(BasePeriodicTaskAdmin, ModelAdmin):
        pass

Pattern 5: Dynamic Sidebar Badges

# utils.py
def pending_orders_badge(request):
    count = Order.objects.filter(status="pending").count()
    return str(count) if count > 0 else None
# settings.py
"SIDEBAR": {
    "navigation": [{
        "items": [{
            "title": "Orders",
            "badge": "myapp.utils.pending_orders_badge",
            "badge_variant": "danger",
        }],
    }],
}

Pattern 6: Environment-Aware Configuration

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:

@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

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

# 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

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):

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

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