Package: django-unfold
Unfold Django Admin ThemeUnfold is theme for Django admin incorporating most common practises for building full-fledged admin areas. It is designed to work at the top of default administration provided by Django.Unfold:demo site is available atunfoldadmin.comFormula:repository with demo implementation atgithub.com/unfoldadmin/formulaTurbo:Django & Next.js boilerplate implementing Unfold atgithub.com/unfoldadmin/turboAre you using Unfold and need a help?Did you decide to start using Unfold but you don't have time to make the switch from native Django admin?Get in touch with usand let's supercharge development by using our know-how.FeaturesVisual: provides new user interface based on Tailwind CSS frameworkSidebar:simplifies definition of custom sidebar navigation with iconsDark mode:supports both light and dark mode versionsConfiguration:most of the basic options can be changed in settings.pyDependencies:completely based only ondjango.contrib.adminActions:multiple ways how to define actions within different parts of adminWYSIWYG:built-in support for WYSIWYG (Trix)Custom filters:widgets for filtering number & datetime valuesDashboard:custom components for rapid dashboard developmentModel tabs:define custom tab navigations for modelsFieldset tabs:merge several fielsets into tabs in change formColors:possibility to override default color schemeThird party packages:default support for multiple popular applicationsEnvironment label: distinguish between environments by displaying a labelTable of contentsInstallationConfigurationAvailable settings.py optionsAvailable unfold.admin.ModelAdmin optionsActionsActions overviewCustom unfold @action decoratorAction handler functionsAction examplesFiltersNumeric filtersDate/time filtersDisplay decoratorChange form tabsThird party packagesdjango-celery-beatdjango-guardiandjango-import-exportdjango-modeltranslationdjango-moneydjango-simple-historyUser Admin FormAdding custom styles and scriptsProject level Tailwind stylesheetAdmin dashboardOverriding templateCustom variablesUnfold componentsUnfold developmentPre-commitPoetry configurationCompiling TailwindCreditsInstallationThe installation process is minimal. Everything what is needed after installation is to put new application at the beginning ofINSTALLED_APPS. Default admin configuration in urls.py can stay as it is and there are no changes required.# settings.pyINSTALLED_APPS=["unfold",# before django.contrib.admin"unfold.contrib.filters",# optional, if special filters are needed"unfold.contrib.forms",# optional, if special form elements are needed"unfold.contrib.import_export",# optional, if django-import-export package is used"unfold.contrib.guardian",# optional, if django-guardian package is used"unfold.contrib.simple_history",# optional, if django-simple-history package is used"django.contrib.admin",# required]In case you need installation command below are the versions forpipandpoetrywhich needs to be executed in shell.pipinstalldjango-unfold
poetryadddjango-unfoldJust for an example below is the minimal admin configuration in terms of adding Unfold into URL paths.# urls.pyfromdjango.contribimportadminfromdjango.urlsimportpathurlpatterns=[path("admin/",admin.site.urls),# Other URL paths]After installation, it is required that admin classes are going to inherit from customModelAdminavailable inunfold.admin.# admin.pyfromdjango.contribimportadminfromunfold.adminimportModelAdmin@admin.register(MyModel)classCustomAdminClass(ModelAdmin):passNote:Registered admin models coming from third party packages are not going to properly work with Unfold because of parent class. By default, these models are registered by usingdjango.contrib.admin.ModelAdminbut it is needed to useunfold.admin.ModelAdmin. Solution for this problem is to unregister model and then again register it back by usingunfold.admin.ModelAdmin.# admin.pyfromdjango.contribimportadminfromdjango.contrib.auth.adminimportUserAdminasBaseUserAdminfromdjango.contrib.auth.modelsimportUserfromunfold.adminimportModelAdminadmin.site.unregister(User)@admin.register(User)classUserAdmin(BaseUserAdmin,ModelAdmin):passConfigurationAvailable settings.py options# settings.pyfromdjango.templatetags.staticimportstaticfromdjango.urlsimportreverse_lazyfromdjango.utils.translationimportgettext_lazyas_UNFOLD={"SITE_TITLE":None,"SITE_HEADER":None,"SITE_URL":"/",# "SITE_ICON": lambda request: static("icon.svg"),  # both modes, optimise for 32px height"SITE_ICON":{"light":lambdarequest:static("icon-light.svg"),# light mode"dark":lambdarequest:static("icon-dark.svg"),# dark mode},# "SITE_LOGO": lambda request: static("logo.svg"),  # both modes, optimise for 32px height"SITE_LOGO":{"light":lambdarequest:static("logo-light.svg"),# light mode"dark":lambdarequest:static("logo-dark.svg"),# dark mode},"SITE_SYMBOL":"speed",# symbol from icon set"SHOW_HISTORY":True,# show/hide "History" button, default: True"SHOW_VIEW_ON_SITE":True,# show/hide "View on site" button, default: True"ENVIRONMENT":"sample_app.environment_callback","DASHBOARD_CALLBACK":"sample_app.dashboard_callback","LOGIN":{"image":lambdarequest:static("sample/login-bg.jpg"),"redirect_after":lambdarequest:reverse_lazy("admin:APP_MODEL_changelist"),},"STYLES":[lambdarequest:static("css/style.css"),],"SCRIPTS":[lambdarequest:static("js/script.js"),],"COLORS":{"primary":{"50":"250 245 255","100":"243 232 255","200":"233 213 255","300":"216 180 254","400":"192 132 252","500":"168 85 247","600":"147 51 234","700":"126 34 206","800":"107 33 168","900":"88 28 135","950":"59 7 100",},},"EXTENSIONS":{"modeltranslation":{"flags":{"en":"🇬🇧","fr":"🇫🇷","nl":"🇧🇪",},},},"SIDEBAR":{"show_search":False,# Search in applications and models names"show_all_applications":False,# Dropdown with all applications and models"navigation":[{"title":_("Navigation"),"separator":True,# Top border"items":[{"title":_("Dashboard"),"icon":"dashboard",# Supported icon set: https://fonts.google.com/icons"link":reverse_lazy("admin:index"),"badge":"sample_app.badge_callback","permission":lambdarequest:request.user.is_superuser,},{"title":_("Users"),"icon":"people","link":reverse_lazy("admin:users_user_changelist"),},],},],},"TABS":[{"models":["app_label.model_name_in_lowercase",],"items":[{"title":_("Your custom title"),"link":reverse_lazy("admin:app_label_model_name_changelist"),"permission":"sample_app.permission_callback",},],},],}defdashboard_callback(request,context):"""Callback to prepare custom variables for index template which is used as dashboardtemplate. It can be overridden in application by creating custom admin/index.html."""context.update({"sample":"example",# this will be injected into templates/admin/index.html})returncontextdefenvironment_callback(request):"""Callback has to return a list of two values represeting text value and the colortype of the label displayed in top right corner."""return["Production","danger"]# info, danger, warning, successdefbadge_callback(request):return3defpermission_callback(request):returnrequest.user.has_perm("sample_app.change_model")Available unfold.admin.ModelAdmin options# admin.pyfromdjangoimportmodelsfromdjango.contribimportadminfromdjango.dbimportmodelsfromunfold.adminimportModelAdminfromunfold.contrib.forms.widgetsimportWysiwygWidget@admin.register(MyModel)classCustomAdminClass(ModelAdmin):# Preprocess content of readonly fields before renderreadonly_preprocess_fields={"model_field_name":"html.unescape","other_field_name":lambdacontent:content.strip(),}# Display submit button in filterslist_filter_submit=False# Custom actionsactions_list=[]# Displayed above the results listactions_row=[]# Displayed in a table row in results listactions_detail=[]# Displayed at the top of for in object detailactions_submit_line=[]# Displayed near save in object detailformfield_overrides={models.TextField:{"widget":WysiwygWidget,}}ActionsIt is highly recommended to read the baseDjango actions documentationbefore reading this section, since Unfold actions are derived from Django actions.Actions overviewBesides traditional actions selected from dropdown, Unfold supports several other types of actions. Following table
gives overview of all available actions together with their recommended usage:Type of actionAppearanceUsageExamples of usageDefaultList view - top of listing (in dropdown)Actions, where you want to select specific subset of instances to perform this action uponBulk deleting, bulk activationGlobalList view - top of listing (as buttons)General actions for model, without selecting specific instancesImport, exportRowList view - in each rowAction for one specific instance, executable from listingActivation, sync with external serviceDetailDetail view - top of detailAction for one specific instance, executable from detailActivation, sync with external serviceSubmit lineDetail view - near submit buttonAction performed during form submit (instance save)Publishing article together with saveCustom unfold @action decoratorUnfold also uses custom@actiondecorator, supporting 2 more parameters in comparison to base@actiondecorator:url_path: Action path name, used to override the path under which the action will be available
(if not provided, URL path will be generated by Unfold)attrs: Dictionary of the additional attributes added to the<a>element, used for e.g. opening action in new tab ({"target": "_blank"})Action handler functionsThis section provides explanation of how the action handler functions should be constructed for Unfold actions.
For default actions, follow official Django admin documentation.For submit row actionSubmit row actions work a bit differently when compared to other custom Unfold actions.
These actions first invoke form save (same as if you hitSavebutton) and then lets you
perform additional logic on already saved instance.For global, row and detail actionAll these actions are based on custom URLs generated for each of them. Handler function for these views is
basically function based view.For actions without intermediate steps, you can write all the logic inside handler directly. Request and object ID
are both passed to these action handler functions, so you are free to fetch the instance from database and perform any
operations with it. In the end, it is recommended to return redirect back to either detail or listing, based on where
the action was triggered from.For actions with intermediate steps, it is recommended to use handler function only to redirect to custom URL with custom
view. This view can be extended from base Unfold view, to have unified experience.If that's confusing, there are examples for both these actions in next section.Action examples# admin.pyfromdjango.db.modelsimportModelfromdjango.contrib.adminimportregisterfromdjango.shortcutsimportredirectfromdjango.urlsimportreverse_lazyfromdjango.utils.translationimportgettext_lazyas_fromdjango.httpimportHttpRequestfromunfold.adminimportModelAdminfromunfold.decoratorsimportactionclassUser(Model):pass@register(User)classUserAdmin(ModelAdmin):actions_list=["changelist_global_action_import"]actions_row=["changelist_row_action_view_on_website"]actions_detail=["change_detail_action_block"]actions_submit_line=["submit_line_action_activate"]@action(description=_("Save & Activate"))defsubmit_line_action_activate(self,request:HttpRequest,obj:User):"""If instance is modified in any way, it also needs to be saved,since this handler is invoked after instance is saved.:param request::param obj: Model instance that was manipulated, with changes already saved to database:return: None, this handler should not return anything"""obj.is_active=Trueobj.save()@action(description=_("Import"),url_path="import")defchangelist_global_action_import(self,request:HttpRequest):"""Handler for global actions does not receive any queryset or object ids, because it ismeant to be used for general actions for given model.:param request::return: View, as described in section above"""# This is example of action redirecting to custom page, where the action will be handled# (with intermediate steps)returnredirect(reverse_lazy("view-where-import-will-be-handled"))@action(description=_("Row"),url_path="row-action",attrs={"target":"_blank"})defchangelist_row_action_view_on_website(self,request:HttpRequest,object_id:int):"""Handler for list row action.:param request::param object_id: ID of instance that this action was invoked for:return: View, as described in section above"""returnredirect(f"https://example.com/{object_id}")@action(description=_("Detail"),url_path="detail-action",attrs={"target":"_blank"})defchange_detail_action_block(self,request:HttpRequest,object_id:int):"""Handler for detail action.:param request::param object_id: ID of instance that this action was invoked for:return: View, as described in section above"""# This is example of action that handled whole logic inside handler# function and redirects back to object detailuser=User.objects.get(pk=object_id)user.block()returnredirect(reverse_lazy("admin:users_user_change",args=(object_id,)))FiltersBy default, Django admin handles all filters as regular HTML links pointing at the same URL with different query parameters. This approach is for basic filtering more than enough. In the case of more advanced filtering by incorporating input fields, it is not going to work.Note:when implementing a filter which contains input fields, there is a no way that user can submit the values, because default filters does not contain submit button. To implement submit button,unfold.admin.ModelAdmincontains booleanlist_filter_submitflag which enables submit button in filter form.Numeric filtersCurrently, Unfold implements numeric filters insideunfold.contrib.filtersapplication. In order to use these filters, it is required to add this application intoINSTALLED_APPSinsettings.pyright afterunfoldapplication.# admin.pyfromdjango.contribimportadminfromdjango.contrib.auth.modelsimportUserfromunfold.adminimportModelAdminfromunfold.contrib.filters.adminimport(RangeNumericListFilter,RangeNumericFilter,SingleNumericFilter,SliderNumericFilter,)classCustomSliderNumericFilter(SliderNumericFilter):MAX_DECIMALS=2STEP=10classCustomRangeNumericListFilter(RangeNumericListFilter):parameter_name="items_count"title="items"@admin.register(User)classYourModelAdmin(ModelAdmin):list_filter_submit=True# Submit button at the bottom of the filterlist_filter=(("field_A",SingleNumericFilter),# Numeric single field search, __gte lookup("field_B",RangeNumericFilter),# Numeric range search, __gte and __lte lookup("field_C",SliderNumericFilter),# Numeric range filter but with slider("field_D",CustomSliderNumericFilter),# Numeric filter with custom attributesCustomRangeNumericListFilter,# Numeric range search not restricted to a model field)defget_queryset(self,request):returnsuper().get_queryset().annotate(items_count=Count("item",distinct=True))Date/time filters# admin.pyfromdjango.contribimportadminfromdjango.contrib.auth.modelsimportUserfromunfold.adminimportModelAdminfromunfold.contrib.filters.adminimport(RangeDateFilter,RangeDateTimeFilter,)@admin.register(User)classYourModelAdmin(ModelAdmin):list_filter_submit=True# Submit button at the bottom of the filterlist_filter=(("field_E",RangeDateFilter),# Date filter("field_F",RangeDateTimeFilter),# Datetime filter)Display decoratorUnfold introduces it's ownunfold.decorators.displaydecorator. By default it has exactly same behavior as nativedjango.contrib.admin.decorators.displaybut it adds same customizations which helps to extends default logic.@display(label=True),@display(label={"value1": "success"})displays a result as a label. This option fits for different types of statuses. Label can be either boolean indicating we want to use label with default color or dict where the dict is responsible for displaying labels in different colors. At the moment these color combinations are supported: success(green), info(blue), danger(red) and warning(orange).@display(header=True)displays in results list two information in one table cell. Good example is when we want to display customer information, first line is going to be customer's name and right below the name display corresponding email address. Method with such a decorator is supposed to return a list with two elementsreturn "Full name", "E-mail address". There is a third optional argument, which is type of the string and its value is displayed in a circle before first two values on the front end. Its optimal usage is for displaying initials.# admin.pyfromdjango.db.modelsimportTextChoicesfromdjango.utils.translationimportgettext_lazyas_fromunfold.adminimportModelAdminfromunfold.decoratorsimportdisplayclassUserStatus(TextChoices):ACTIVE="ACTIVE",_("Active")PENDING="PENDING",_("Pending")INACTIVE="INACTIVE",_("Inactive")CANCELLED="CANCELLED",_("Cancelled")classUserAdmin(ModelAdmin):list_display=["display_as_two_line_heading","show_status","show_status_with_custom_label",]@display(description=_("Status"),ordering="status",label=True)defshow_status_default_color(self,obj):returnobj.status@display(description=_("Status"),ordering="status",label={UserStatus.ACTIVE:"success",# greenUserStatus.PENDING:"info",# blueUserStatus.INACTIVE:"warning",# orangeUserStatus.CANCELLED:"danger",# red},)defshow_status_customized_color(self,obj):returnobj.status@display(description=_("Status with label"),ordering="status",label=True)defshow_status_with_custom_label(self,obj):returnobj.status,obj.get_status_display()@display(header=True)defdisplay_as_two_line_heading(self,obj):"""Third argument is short text which will appear as prefix in circle"""return"First main heading","Smaller additional description","AB"Change form tabsWhen the change form contains a lot of fieldsets, sometimes it is better to group them into tabs so it will not be needed to scroll. To mark a fieldset for tab navigation it is required to add atabCSS class to the fieldset. Once the fieldset containstabclass it will be recognized in a template and grouped into tab navigation. Each tab must contain its name. If the name is not available, it will be not included in the tab navigation.# admin.pyfromdjango.contribimportadminfromdjango.utils.translationimportgettext_lazyas_fromunfold.adminimportModelAdminfrom.modelsimportMyModel@admin.register(MyModel)classMyModelAdmin(ModelAdmin):fieldsets=((None,{"fields":["field_1","field_2",],},),(_("Tab 1"),{"classes":["tab"],"fields":["field_3","field_4",],},),(_("Tab 2"),{"classes":["tab"],"fields":["field_5","field_6",],},),)Third party packagesdjango-celery-beatIn general, django-celery-beat does not have any components that require special styling. The default changelist templates are not inheriting from Unfold'sModelAdminbut they are using defaultModelAdmincoming fromdjango.contrib.adminwhich is causing some design discrepancies in the changelist.In the source code below you can find a short code snippet to unregister alldjango-celery-beatadmin classes and register them with the proper parentModelAdminclass.# admin.pyfromdjango.contribimportadminfromunfold.adminimportModelAdminfromdjango_celery_beat.modelsimport(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)@admin.register(PeriodicTask)classPeriodicTaskAdmin(ModelAdmin):pass@admin.register(IntervalSchedule)classIntervalScheduleAdmin(ModelAdmin):pass@admin.register(CrontabSchedule)classCrontabScheduleAdmin(ModelAdmin):pass@admin.register(SolarSchedule)classSolarScheduleAdmin(ModelAdmin):passdjango-guardianAdding support for django-guardian is quote straightforward in Unfold, just addunfold.contrib.guardiantoINSTALLED_APPSat the beggining of the file. This action will override all templates coming from the django-guardian. Please note thatObject permissionslink is available in top right dropdown navigation.django-import-exportAddunfold.contrib.import_exporttoINSTALLED_APPSat the beggining of the file. This action will override all templates coming from the application.Changeimport_form_classandexport_form_classin ModelAdmin which is inheriting fromImportExportModelAdmin. This chunk of code is responsible for adding proper styling to form elements.# admin.pyfromunfold.adminimportModelAdminfromimport_export.adminimportImportExportModelAdminfromunfold.contrib.import_export.formsimportExportForm,ImportFormclassExampleAdmin(ModelAdmin,ImportExportModelAdmin):import_form_class=ImportFormexport_form_class=ExportFormWhen implementingimport_export.admin.ExportActionModelAdminclass in admin panel, import_export plugin adds its own implementation of action form which is not incorporating Unfold CSS classes. For this reason,unfold.contrib.import_export.admincontains class with the same nameExportActionModelAdminwhich inherits behavior of parent form and adds appropriate CSS classes.admin.pyfromunfold.adminimportModelAdminfromunfold.contrib.import_exportimportExportActionModelAdminclassExampleAdmin(ModelAdmin,ExportActionModelAdmin):passdjango-modeltranslationBy default, Unfold supports django-modeltranslation andTabbedTranslationAdminadmin class for the tabbed navigation is implemented with custom styling as well.fromdjango.contribimportadminfrommodeltranslation.adminimportTabbedTranslationAdminfromunfold.adminimportModelAdminfrom.modelsimportMyModel@admin.register(MyModel)classMyModelAdmin(ModelAdmin,TabbedTranslationAdmin):passFor django-modeltranslation fields for spefic languages, it is possible to define custom flags which will appear as a suffix in field's label. It is recommended to use emojis as suffix.# settings.pyUNFOLD={"EXTENSIONS":{"modeltranslation":{"flags":{"en":"🇬🇧","fr":"🇫🇷","nl":"🇧🇪",},},},}django-moneyThis application is supported in Unfold by default. It is not needed to add any other applications intoINSTALLED_APPS. Unfold is recognizing special form widget coming from django-money and applying specific styling.django-simple-historyTo make this application work, addunfold.contrib.simple_historyintosettings.pyinINSTALLED_APPSvariable before right afterunfold. This app should ensure that templates coming from django-simple-history are overriden by Unfold.User Admin FormUser's admin in Django is specific as it contains several forms which are requiring custom styling. All of these forms has been inherited and accordingly adjusted. In user admin class it is needed to use these inherited form classes to enable custom styling matching rest of the website.# models.pyfromdjango.contrib.adminimportregisterfromdjango.contrib.auth.modelsimportUserfromdjango.contrib.auth.adminimportUserAdminasBaseUserAdminfromunfold.adminimportModelAdminfromunfold.formsimportAdminPasswordChangeForm,UserChangeForm,UserCreationForm@register(User)classUserAdmin(BaseUserAdmin,ModelAdmin):form=UserChangeFormadd_form=UserCreationFormchange_password_form=AdminPasswordChangeFormAdding custom styles and scriptsTo add new custom styles, for example for custom dashboard, it is possible to load them viaSTYLESkey inUNFOLDdict. This key accepts a list of strings or lambda functions which will be loaded on all pages. JavaScript files can be loaded by using similar apprach, butSCRIPTSis used.# settings.pyfromdjango.templatetags.staticimportstaticUNFOLD={"STYLES":[lambdarequest:static("css/style.css"),],"SCRIPTS":[lambdarequest:static("js/script.js"),],}Project level Tailwind stylesheetWhen creating custom dashboard or adding custom components, it is needed to add own styles. Adding custom styles is described above. Most of the time, it is supposed that new elements are going to match with the rest of the administration panel. First of all, create tailwind.config.js in your application. Below is located minimal configuration for this file.// tailwind.config.jsmodule.exports={content:["./your_project/**/*.{html,py,js}"],// In case custom colors are defined in UNFOLD["COLORS"]colors:{primary:{50:"rgb(var(--color-primary-50) / <alpha-value>)",100:"rgb(var(--color-primary-100) / <alpha-value>)",200:"rgb(var(--color-primary-200) / <alpha-value>)",300:"rgb(var(--color-primary-300) / <alpha-value>)",400:"rgb(var(--color-primary-400) / <alpha-value>)",500:"rgb(var(--color-primary-500) / <alpha-value>)",600:"rgb(var(--color-primary-600) / <alpha-value>)",700:"rgb(var(--color-primary-700) / <alpha-value>)",800:"rgb(var(--color-primary-800) / <alpha-value>)",900:"rgb(var(--color-primary-900) / <alpha-value>)",950:"rgb(var(--color-primary-950) / <alpha-value>)",},},};Once the configuration file is set, it is possible to compile new styles which can be loaded into admin by usingSTYLESkey inUNFOLDdict.npxtailwindcss-oyour_project/static/css/styles.css--watch--minifyAdmin dashboardOverriding templateCreatetemplates/admin/index.htmlin your project and paste the base template below into it. By default, all your custom styles here are not compiled because CSS classes are located in your specific project. Here it is needed to set up the Tailwind for your project and all requried instructions are located inProject Level Tailwind Stylesheetchapter.{%extends'unfold/layouts/base_simple.html'%}{%loadcachehumanizei18n%}{%blockbreadcrumbs%}{%endblock%}{%blocktitle%}{%ifsubtitle%}{{subtitle}}|{%endif%}{{title}}|{{site_title|default:_('Django site admin')}}{%endblock%}{%blockbranding%}<h1id="site-name"><ahref="{%url'admin:index'%}">{{site_header|default:_('Django administration')}}</a></h1>{%endblock%}{%blockcontent%}Start creating your own Tailwind components here{%endblock%}Custom variablesWhen you are building a new dashboard, you need to display some data mostly coming from the database. To pass these data to the dashboard template, Unfold contains a specialDASHBOARD_CALLBACKparameter which allows passing a dictionary of variables totemplates/admin/index.htmltemplate.# views.pydefdashboard_callback(request,context):context.update({"custom_variable":"value",})returncontext# settings.pyUNFOLD={"DASHBOARD_CALLBACK":"app.views.dashboard_callback",}Unfold componentsUnfold provides a set of already predefined templates to speed up overall dashboard development. These templates contain predefined design which matches global design style so there is no need to spend any time adjusting styles.The biggest benefit of Unfold components is the possibility to nest them inside one template file provides an unlimited amount of possible combinations. Then each component includeschildrenvariable which contains a value specified in the parent component. Except forchildrenvariable, components can have multiple variables coming from the parent template as component variables. These parameters can be specified in the same as parameters when using{% include with param1=value1 param2=value2 %}template tag.{%component"unfold/components/flex.html"withcol=1%}{%component"unfold/components/card.html"%}{%component"unfold/components/title.html"%}Card Title{%endcomponent%}{%endcomponent%}{%endcompontent%}Below you can find a more complex example which is using multiple components and processing them based on the passed variables from theDASHBOARD_CALLBACK.{%loadi18n%}{%blockcontent%}{%component"unfold/components/container.html"%}{%component"unfold/components/flex.html"withclass="gap-4"%}{%component"unfold/components/navigation.html"withitems=navigation%}{%endcomponent%}{%component"unfold/components/navigation.html"withclass="ml-auto"items=filters%}{%endcomponent%}{%endcomponent%}{%component"unfold/components/flex.html"withclass="gap-8 mb-8 flex-col lg:flex-row"%}{%forcardincards%}{%trans"Last 7 days"aslabel%}{%component"unfold/components/card.html"withclass="lg:w-1/3"%}{%component"unfold/components/text.html"%}{{card.title}}{%endcomponent%}{%component"unfold/components/title.html"%}{{card.metric}}{%endcomponent%}{%endcomponent%}{%endfor%}{%endcomponent%}{%endcomponent%}{%endblock%}List of available componentsComponentDescriptionArgumentsunfold/components/chart/bar.htmlBar chart implementationclass, data, height, widthunfold/components/chart/line.htmlLine chart implementationclass, data, height, widthunfold/components/card.htmlCard componentclass, title, footer, labelunfold/components/container.htmlWrapper for settings max widthclassunfold/components/flex.htmlFlex itemsclass, colunfold/components/navigation.htmlList of navigation linksclass, itemsunfold/components/progress.htmlPercentual progress barclass, value, title, descriptionunfold/components/separator.htmlSeparator, horizontal ruleclassunfold/components/text.htmlParagraph of textclassunfold/components/title.htmlBasic heading elementclassUnfold developmentPre-commitBefore adding any source code, it is recommended to have pre-commit installed on your local computer to check for all potential issues when comitting the code.pipinstallpre-commit
pre-commitinstall
pre-commitinstall--hook-typecommit-msgPoetry configurationTo add a new feature or fix the easiest approach is to use django-unfold in combination with Poetry. The process looks like:Install django-unfold viapoetry add django-unfoldAfter that it is needed to git clone the repository somewhere on local computer.Editpyproject.tomland update django-unfold linedjango-unfold = { path = "../django-unfold", develop = true}Lock and update viapoetry lock && poetry updateCompiling TailwindAt the moment project contains package.json with all dependencies required to compile new CSS file. Tailwind configuration file is set to check all html, js and py files for Tailwind's classeses occurrences.npminstall
npxtailwindcss-isrc/unfold/styles.css-osrc/unfold/static/unfold/css/styles.css--watch--minify

npmruntailwind:watch# run after each change in codenpmruntailwind:build# run onceSome components like datepickers, calendars or selectors in admin was not possible to style by overriding html templates so their default styles are overriden instyles.css.Note:most of the custom styles located in style.css are created via@apply some-tailwind-class;as is not possible to manually add CSS class to element which are for example created via jQuery.CreditsTailwindCSS- CSS frameworkHTMX- AJAX communication with backendMaterial Icons- Icons from Google FontsTrix- WYSIWYG editorAlpine.js- JavaScript interactions