django-unfold 0.37.0__py3-none-any.whl → 0.39.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. django_unfold-0.39.0.dist-info/METADATA +70 -0
  2. {django_unfold-0.37.0.dist-info → django_unfold-0.39.0.dist-info}/RECORD +59 -57
  3. unfold/admin.py +4 -1
  4. unfold/contrib/filters/templates/unfold/filters/filters_date_range.html +1 -1
  5. unfold/contrib/filters/templates/unfold/filters/filters_datetime_range.html +1 -1
  6. unfold/contrib/filters/templates/unfold/filters/filters_numeric_range.html +1 -1
  7. unfold/contrib/filters/templates/unfold/filters/filters_numeric_single.html +1 -1
  8. unfold/contrib/filters/templates/unfold/filters/filters_numeric_slider.html +4 -4
  9. unfold/contrib/forms/templates/unfold/forms/helpers/toolbar.html +6 -6
  10. unfold/contrib/guardian/templates/unfold/guardian/group_form.html +5 -5
  11. unfold/contrib/guardian/templates/unfold/guardian/user_form.html +5 -5
  12. unfold/contrib/import_export/templates/admin/import_export/change_form.html +1 -1
  13. unfold/contrib/import_export/templates/admin/import_export/import_errors.html +1 -1
  14. unfold/contrib/simple_history/templates/simple_history/object_history.html +1 -1
  15. unfold/contrib/simple_history/templates/simple_history/object_history_list.html +8 -8
  16. unfold/contrib/simple_history/templates/simple_history/submit_line.html +1 -1
  17. unfold/forms.py +5 -1
  18. unfold/settings.py +26 -1
  19. unfold/static/admin/js/admin/RelatedObjectLookups.js +295 -0
  20. unfold/static/unfold/css/styles.css +1 -1
  21. unfold/static/unfold/js/app.js +3 -1
  22. unfold/styles.css +1 -1
  23. unfold/templates/admin/app_list.html +2 -2
  24. unfold/templates/admin/change_form.html +8 -1
  25. unfold/templates/admin/change_list.html +1 -6
  26. unfold/templates/admin/change_list_results.html +2 -2
  27. unfold/templates/admin/delete_confirmation.html +5 -5
  28. unfold/templates/admin/delete_selected_confirmation.html +5 -5
  29. unfold/templates/admin/edit_inline/stacked.html +2 -2
  30. unfold/templates/admin/edit_inline/tabular.html +3 -3
  31. unfold/templates/admin/filter.html +1 -1
  32. unfold/templates/admin/includes/fieldset.html +1 -1
  33. unfold/templates/admin/includes/object_delete_summary.html +1 -1
  34. unfold/templates/admin/login.html +2 -2
  35. unfold/templates/admin/search_form.html +1 -1
  36. unfold/templates/auth/widgets/read_only_password_hash.html +2 -2
  37. unfold/templates/registration/logged_out.html +1 -1
  38. unfold/templates/unfold/change_list_filter.html +19 -3
  39. unfold/templates/unfold/components/button.html +1 -1
  40. unfold/templates/unfold/components/card.html +1 -1
  41. unfold/templates/unfold/components/navigation.html +1 -1
  42. unfold/templates/unfold/components/title.html +1 -1
  43. unfold/templates/unfold/helpers/actions_row.html +1 -1
  44. unfold/templates/unfold/helpers/app_list.html +7 -7
  45. unfold/templates/unfold/helpers/fieldsets_tabs.html +2 -2
  46. unfold/templates/unfold/helpers/form_label.html +1 -1
  47. unfold/templates/unfold/helpers/messages.html +1 -1
  48. unfold/templates/unfold/helpers/navigation.html +1 -1
  49. unfold/templates/unfold/helpers/search.html +1 -1
  50. unfold/templates/unfold/helpers/tab_action.html +1 -1
  51. unfold/templates/unfold/helpers/welcomemsg.html +3 -3
  52. unfold/templates/unfold/layouts/skeleton.html +1 -1
  53. unfold/templates/unfold/widgets/clearable_file_input.html +2 -2
  54. unfold/templates/unfold/widgets/clearable_file_input_small.html +1 -1
  55. unfold/templates/unfold/widgets/url.html +7 -0
  56. unfold/templatetags/unfold_list.py +0 -7
  57. unfold/widgets.py +26 -6
  58. django_unfold-0.37.0.dist-info/METADATA +0 -1455
  59. {django_unfold-0.37.0.dist-info → django_unfold-0.39.0.dist-info}/LICENSE.md +0 -0
  60. {django_unfold-0.37.0.dist-info → django_unfold-0.39.0.dist-info}/WHEEL +0 -0
@@ -1,1455 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: django-unfold
3
- Version: 0.37.0
4
- Summary: Modern Django admin theme for seamless interface development
5
- Home-page: https://unfoldadmin.com
6
- License: MIT
7
- Keywords: django,admin,tailwind,theme
8
- Requires-Python: >=3.8
9
- Classifier: Environment :: Web Environment
10
- Classifier: Framework :: Django
11
- Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Operating System :: OS Independent
14
- Classifier: Programming Language :: Python
15
- Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.8
17
- Classifier: Programming Language :: Python :: 3.9
18
- Classifier: Programming Language :: Python :: 3.10
19
- Classifier: Programming Language :: Python :: 3.11
20
- Classifier: Programming Language :: Python :: 3.12
21
- Requires-Dist: django (>=3.2)
22
- Project-URL: Repository, https://github.com/unfoldadmin/django-unfold
23
- Description-Content-Type: text/markdown
24
-
25
- [![screenshot-light](https://github.com/unfoldadmin/django-unfold/assets/10785882/291e69c9-abdd-4f7e-a0d6-2af210a9013a#gh-light-mode-only)](https://github.com/unfoldadmin/django-unfold/assets/10785882/291e69c9-abdd-4f7e-a0d6-2af210a9013a#gh-light-mode-only)
26
-
27
- [![screenshot-dark](https://github.com/unfoldadmin/django-unfold/assets/10785882/94a2e90f-924a-4aaf-b6d9-cb1592000c55#gh-dark-mode-only)](https://github.com/unfoldadmin/django-unfold/assets/10785882/94a2e90f-924a-4aaf-b6d9-cb1592000c55#gh-dark-mode-only)
28
-
29
- ## Unfold Django Admin Theme <!-- omit from toc -->
30
-
31
- [![Build](https://img.shields.io/github/actions/workflow/status/unfoldadmin/django-unfold/release.yml?style=for-the-badge)](https://github.com/unfoldadmin/django-unfold/actions?query=workflow%3Arelease)
32
- [![PyPI - Version](https://img.shields.io/pypi/v/django-unfold.svg?style=for-the-badge)](https://pypi.org/project/django-unfold/)
33
- ![Code Style - Ruff](https://img.shields.io/badge/code%20style-ruff-30173D.svg?style=for-the-badge)
34
- ![Pre Commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=for-the-badge)
35
-
36
- Unfold is a theme for Django admin incorporating most common practices for building full-fledged admin areas. It is designed to work on top of default administration provided by Django.
37
-
38
- - **Unfold:** demo site is available at [unfoldadmin.com](https://unfoldadmin.com?utm_medium=github&utm_source=unfold)
39
- - **Formula:** repository with demo implementation at [github.com/unfoldadmin/formula](https://github.com/unfoldadmin/formula)
40
- - **Turbo:** Django & Next.js boilerplate implementing Unfold at [github.com/unfoldadmin/turbo](https://github.com/unfoldadmin/turbo)
41
-
42
- ## Are you using Unfold and need a help?<!-- omit from toc -->
43
-
44
- 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 us](https://unfoldadmin.com/?utm_medium=github&utm_source=unfold) and let's supercharge development by using our know-how.
45
-
46
- ## Features <!-- omit from toc -->
47
-
48
- - **Visual**: provides a new user interface based on Tailwind CSS framework
49
- - **Sidebar:** simplifies definition of custom sidebar navigation with icons
50
- - **Dark mode:** supports both light and dark mode versions
51
- - **Configuration:** most of the basic options can be changed in settings.py
52
- - **Dependencies:** completely based only on `django.contrib.admin`
53
- - **Actions:** multiple ways how to define actions within different parts of admin
54
- - **WYSIWYG:** built-in support for WYSIWYG (Trix)
55
- - **Array widget:** built-in widget for `django.contrib.postgres.fields.ArrayField`
56
- - **Filters:** custom dropdown, numeric, datetime, and text fields
57
- - **Dashboard:** custom components for rapid dashboard development
58
- - **Inline tabs:** group inlines into tab navigation in the change form
59
- - **Model tabs:** define custom tab navigations for models
60
- - **Fieldset tabs:** merge several fieldsets into tabs in the change form
61
- - **Colors:** possibility to override the default color scheme
62
- - **Changeform modes:** display fields in the change form in compressed mode
63
- - **Third party packages:** default support for multiple popular applications
64
- - **Environment label**: distinguish between environments by displaying a label
65
- - **Nonrelated inlines**: displays nonrelated model as inline in changeform
66
- - **Parallel admin**: support for default admin in parallel with Unfold. [Admin migration guide](https://unfoldadmin.com/blog/migrating-django-admin-unfold/?utm_medium=github&utm_source=unfold)
67
- - **Favicons**: built-in support for configuring various site favicons
68
- - **VS Code**: project configuration and development container is included
69
-
70
- ## Table of contents <!-- omit from toc -->
71
-
72
- - [Installation](#installation)
73
- - [Configuration](#configuration)
74
- - [Available settings.py options](#available-settingspy-options)
75
- - [Available unfold.admin.ModelAdmin options](#available-unfoldadminmodeladmin-options)
76
- - [Actions](#actions)
77
- - [Actions overview](#actions-overview)
78
- - [Custom unfold @action decorator](#custom-unfold-action-decorator)
79
- - [Action handler functions](#action-handler-functions)
80
- - [Action examples](#action-examples)
81
- - [Action with form example](#action-with-form-example)
82
- - [Filters](#filters)
83
- - [Text filters](#text-filters)
84
- - [Dropdown filters](#dropdown-filters)
85
- - [Numeric filters](#numeric-filters)
86
- - [Date/time filters](#datetime-filters)
87
- - [Custom admin pages](#custom-admin-pages)
88
- - [Display decorator](#display-decorator)
89
- - [Change form tabs](#change-form-tabs)
90
- - [Inlines](#inlines)
91
- - [Custom title](#custom-title)
92
- - [Hide title row](#hide-title-row)
93
- - [Display as tabs](#display-as-tabs)
94
- - [Nonrelated inlines](#nonrelated-inlines)
95
- - [Third party packages](#third-party-packages)
96
- - [django-celery-beat](#django-celery-beat)
97
- - [django-guardian](#django-guardian)
98
- - [django-import-export](#django-import-export)
99
- - [django-modeltranslation](#django-modeltranslation)
100
- - [django-money](#django-money)
101
- - [django-simple-history](#django-simple-history)
102
- - [User Admin Form](#user-admin-form)
103
- - [Adding custom styles and scripts](#adding-custom-styles-and-scripts)
104
- - [Project level Tailwind stylesheet](#project-level-tailwind-stylesheet)
105
- - [Admin dashboard](#admin-dashboard)
106
- - [Overriding template](#overriding-template)
107
- - [Custom variables](#custom-variables)
108
- - [Unfold components](#unfold-components)
109
- - [Table component example](#table-component-example)
110
- - [Unfold development](#unfold-development)
111
- - [Pre-commit](#pre-commit)
112
- - [Poetry configuration](#poetry-configuration)
113
- - [Compiling Tailwind](#compiling-tailwind)
114
- - [Design system](#design-system)
115
- - [Using VS Code with containers](#using-vs-code-with-containers)
116
- - [Development server](#development-server)
117
- - [Compiling Tailwind in devcontainer](#compiling-tailwind-in-devcontainer)
118
- - [Credits](#credits)
119
-
120
- ## Installation
121
-
122
- The installation process is minimal. Everything that is needed after installation is to put new application at the beginning of **INSTALLED_APPS**. The default admin configuration in urls.py can stay as it is, and no changes are required.
123
-
124
- ```python
125
- # settings.py
126
-
127
- INSTALLED_APPS = [
128
- "unfold", # before django.contrib.admin
129
- "unfold.contrib.filters", # optional, if special filters are needed
130
- "unfold.contrib.forms", # optional, if special form elements are needed
131
- "unfold.contrib.inlines", # optional, if special inlines are needed
132
- "unfold.contrib.import_export", # optional, if django-import-export package is used
133
- "unfold.contrib.guardian", # optional, if django-guardian package is used
134
- "unfold.contrib.simple_history", # optional, if django-simple-history package is used
135
- "django.contrib.admin", # required
136
- ]
137
- ```
138
-
139
- In case you need installation command below are the versions for `pip` and `poetry` which needs to be executed in shell.
140
-
141
- ```bash
142
- pip install django-unfold
143
- poetry add django-unfold
144
- ```
145
-
146
- Just for an example below is the minimal admin configuration in terms of adding Unfold into URL paths.
147
-
148
- ```python
149
- # urls.py
150
-
151
- from django.contrib import admin
152
- from django.urls import path
153
-
154
- urlpatterns = [
155
- path("admin/", admin.site.urls),
156
- # Other URL paths
157
- ]
158
- ```
159
-
160
- After installation, it is required that admin classes are going to inherit from custom `ModelAdmin` available in `unfold.admin`.
161
-
162
- ```python
163
- # admin.py
164
-
165
- from django.contrib import admin
166
- from unfold.admin import ModelAdmin
167
-
168
-
169
- @admin.register(MyModel)
170
- class CustomAdminClass(ModelAdmin):
171
- pass
172
- ```
173
-
174
- **Note:** 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 using `django.contrib.admin.ModelAdmin` but it is needed to use `unfold.admin.ModelAdmin`. Solution for this problem is to unregister model and then again register it back by using `unfold.admin.ModelAdmin`.
175
-
176
- ```python
177
- # admin.py
178
-
179
- from django.contrib import admin
180
- from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
181
- from django.contrib.auth.admin import GroupAdmin as BaseGroupAdmin
182
- from django.contrib.auth.models import User, Group
183
-
184
- from unfold.admin import ModelAdmin
185
-
186
-
187
- admin.site.unregister(User)
188
- admin.site.unregister(Group)
189
-
190
-
191
- @admin.register(User)
192
- class UserAdmin(BaseUserAdmin, ModelAdmin):
193
- pass
194
-
195
-
196
- @admin.register(Group)
197
- class GroupAdmin(BaseGroupAdmin, ModelAdmin):
198
- pass
199
- ```
200
-
201
- ## Configuration
202
-
203
- ### Available settings.py options
204
-
205
- ```python
206
- # settings.py
207
-
208
- from django.templatetags.static import static
209
- from django.urls import reverse_lazy
210
- from django.utils.translation import gettext_lazy as _
211
-
212
- UNFOLD = {
213
- "SITE_TITLE": None,
214
- "SITE_HEADER": None,
215
- "SITE_URL": "/",
216
- # "SITE_ICON": lambda request: static("icon.svg"), # both modes, optimise for 32px height
217
- "SITE_ICON": {
218
- "light": lambda request: static("icon-light.svg"), # light mode
219
- "dark": lambda request: static("icon-dark.svg"), # dark mode
220
- },
221
- # "SITE_LOGO": lambda request: static("logo.svg"), # both modes, optimise for 32px height
222
- "SITE_LOGO": {
223
- "light": lambda request: static("logo-light.svg"), # light mode
224
- "dark": lambda request: static("logo-dark.svg"), # dark mode
225
- },
226
- "SITE_SYMBOL": "speed", # symbol from icon set
227
- "SITE_FAVICONS": [
228
- {
229
- "rel": "icon",
230
- "sizes": "32x32",
231
- "type": "image/svg+xml",
232
- "href": lambda request: static("favicon.svg"),
233
- },
234
- ],
235
- "SHOW_HISTORY": True, # show/hide "History" button, default: True
236
- "SHOW_VIEW_ON_SITE": True, # show/hide "View on site" button, default: True
237
- "ENVIRONMENT": "sample_app.environment_callback",
238
- "DASHBOARD_CALLBACK": "sample_app.dashboard_callback",
239
- "THEME": "dark", # Force theme: "dark" or "light". Will disable theme switcher
240
- "LOGIN": {
241
- "image": lambda request: static("sample/login-bg.jpg"),
242
- "redirect_after": lambda request: reverse_lazy("admin:APP_MODEL_changelist"),
243
- },
244
- "STYLES": [
245
- lambda request: static("css/style.css"),
246
- ],
247
- "SCRIPTS": [
248
- lambda request: static("js/script.js"),
249
- ],
250
- "COLORS": {
251
- "primary": {
252
- "50": "250 245 255",
253
- "100": "243 232 255",
254
- "200": "233 213 255",
255
- "300": "216 180 254",
256
- "400": "192 132 252",
257
- "500": "168 85 247",
258
- "600": "147 51 234",
259
- "700": "126 34 206",
260
- "800": "107 33 168",
261
- "900": "88 28 135",
262
- "950": "59 7 100",
263
- },
264
- },
265
- "EXTENSIONS": {
266
- "modeltranslation": {
267
- "flags": {
268
- "en": "🇬🇧",
269
- "fr": "🇫🇷",
270
- "nl": "🇧🇪",
271
- },
272
- },
273
- },
274
- "SIDEBAR": {
275
- "show_search": False, # Search in applications and models names
276
- "show_all_applications": False, # Dropdown with all applications and models
277
- "navigation": [
278
- {
279
- "title": _("Navigation"),
280
- "separator": True, # Top border
281
- "collapsible": True, # Collapsible group of links
282
- "items": [
283
- {
284
- "title": _("Dashboard"),
285
- "icon": "dashboard", # Supported icon set: https://fonts.google.com/icons
286
- "link": reverse_lazy("admin:index"),
287
- "badge": "sample_app.badge_callback",
288
- "permission": lambda request: request.user.is_superuser,
289
- },
290
- {
291
- "title": _("Users"),
292
- "icon": "people",
293
- "link": reverse_lazy("admin:users_user_changelist"),
294
- },
295
- ],
296
- },
297
- ],
298
- },
299
- "TABS": [
300
- {
301
- "models": [
302
- "app_label.model_name_in_lowercase",
303
- ],
304
- "items": [
305
- {
306
- "title": _("Your custom title"),
307
- "link": reverse_lazy("admin:app_label_model_name_changelist"),
308
- "permission": "sample_app.permission_callback",
309
- },
310
- ],
311
- },
312
- ],
313
- }
314
-
315
-
316
- def dashboard_callback(request, context):
317
- """
318
- Callback to prepare custom variables for index template which is used as dashboard
319
- template. It can be overridden in application by creating custom admin/index.html.
320
- """
321
- context.update(
322
- {
323
- "sample": "example", # this will be injected into templates/admin/index.html
324
- }
325
- )
326
- return context
327
-
328
-
329
- def environment_callback(request):
330
- """
331
- Callback has to return a list of two values represeting text value and the color
332
- type of the label displayed in top right corner.
333
- """
334
- return ["Production", "danger"] # info, danger, warning, success
335
-
336
-
337
- def badge_callback(request):
338
- return 3
339
-
340
- def permission_callback(request):
341
- return request.user.has_perm("sample_app.change_model")
342
-
343
- ```
344
-
345
- ### Available unfold.admin.ModelAdmin options
346
-
347
- ```python
348
- # admin.py
349
-
350
- from django import models
351
- from django.contrib import admin
352
- from django.contrib.postgres.fields import ArrayField
353
- from django.db import models
354
- from unfold.admin import ModelAdmin
355
- from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget
356
-
357
-
358
- @admin.register(MyModel)
359
- class CustomAdminClass(ModelAdmin):
360
- # Display fields in changeform in compressed mode
361
- compressed_fields = True # Default: False
362
-
363
- # Warn before leaving unsaved changes in changeform
364
- warn_unsaved_form = True # Default: False
365
-
366
- # Preprocess content of readonly fields before render
367
- readonly_preprocess_fields = {
368
- "model_field_name": "html.unescape",
369
- "other_field_name": lambda content: content.strip(),
370
- }
371
-
372
- # Display submit button in filters
373
- list_filter_submit = False
374
-
375
- # Display changelist in fullwidth
376
- list_fullwidth = False
377
-
378
- # Position horizontal scrollbar in changelist at the top
379
- list_horizontal_scrollbar_top = False
380
-
381
- # Dsable select all action in changelist
382
- list_disable_select_all = False
383
-
384
- # Custom actions
385
- actions_list = [] # Displayed above the results list
386
- actions_row = [] # Displayed in a table row in results list
387
- actions_detail = [] # Displayed at the top of for in object detail
388
- actions_submit_line = [] # Displayed near save in object detail
389
-
390
- formfield_overrides = {
391
- models.TextField: {
392
- "widget": WysiwygWidget,
393
- },
394
- ArrayField: {
395
- "widget": ArrayWidget,
396
- }
397
- }
398
- ```
399
-
400
- ## Actions
401
-
402
- It is highly recommended to read the base [Django actions documentation](https://docs.djangoproject.com/en/4.2/ref/contrib/admin/actions/) before reading this section, since Unfold actions are derived from Django actions.
403
-
404
- ### Actions overview
405
-
406
- Besides traditional actions selected from dropdown, Unfold supports several other types of actions. Following table
407
- gives overview of all available actions together with their recommended usage:
408
-
409
- | Type of action | Appearance | Usage | Examples of usage |
410
- | -------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------- |
411
- | Default | List view - top of listing (in dropdown) | Actions, where you want to select specific subset of instances to perform this action upon | Bulk deleting, bulk activation |
412
- | Global | List view - top of listing (as buttons) | General actions for model, without selecting specific instances | Import, export |
413
- | Row | List view - in each row | Action for one specific instance, executable from listing | Activation, sync with external service |
414
- | Detail | Detail view - top of detail | Action for one specific instance, executable from detail | Activation, sync with external service |
415
- | Submit line | Detail view - near submit button | Action performed during form submit (instance save) | Publishing article together with save |
416
-
417
- ### Custom unfold @action decorator
418
-
419
- Unfold also uses custom `@action` decorator, supporting 2 more parameters in comparison to base `@action` decorator:
420
-
421
- - `url_path`: Action path name, used to override the path under which the action will be available
422
- (if not provided, URL path will be generated by Unfold)
423
- - `attrs`: Dictionary of the additional attributes added to the `<a>` element, used for e.g. opening action in new tab (`{"target": "_blank"}`)
424
-
425
- ### Action handler functions
426
-
427
- This section provides explanation of how the action handler functions should be constructed for Unfold actions.
428
- For default actions, follow official Django admin documentation.
429
-
430
- #### For submit row action <!-- omit from toc -->
431
-
432
- Submit row actions work a bit differently when compared to other custom Unfold actions.
433
- These actions first invoke form save (same as if you hit `Save` button) and then lets you
434
- perform additional logic on already saved instance.
435
-
436
- #### For global, row and detail action <!-- omit from toc -->
437
-
438
- All these actions are based on custom URLs generated for each of them. Handler function for these views is
439
- basically function based view.
440
-
441
- For actions without intermediate steps, you can write all the logic inside handler directly. Request and object ID
442
- are both passed to these action handler functions, so you are free to fetch the instance from database and perform any
443
- operations with it. In the end, it is recommended to return redirect back to either detail or listing, based on where
444
- the action was triggered from.
445
-
446
- For actions with intermediate steps, it is recommended to use handler function only to redirect to custom URL with custom
447
- view. This view can be extended from base Unfold view, to have unified experience.
448
-
449
- If that's confusing, there are examples for both these actions in next section.
450
-
451
- ### Action examples
452
-
453
- ```python
454
- # admin.py
455
-
456
- from django.db.models import Model
457
- from django.contrib.admin import register
458
- from django.shortcuts import redirect
459
- from django.urls import reverse_lazy
460
- from django.utils.translation import gettext_lazy as _
461
- from django.http import HttpRequest
462
- from unfold.admin import ModelAdmin
463
- from unfold.decorators import action
464
-
465
-
466
- class User(Model):
467
- pass
468
-
469
-
470
- @register(User)
471
- class UserAdmin(ModelAdmin):
472
- actions_list = ["changelist_global_action_import"]
473
- actions_row = ["changelist_row_action_view_on_website"]
474
- actions_detail = ["change_detail_action_block"]
475
- actions_submit_line = ["submit_line_action_activate"]
476
-
477
- @action(description=_("Save & Activate"), permissions=["submit_line_action_activate"])
478
- def submit_line_action_activate(self, request: HttpRequest, obj: User):
479
- """
480
- If instance is modified in any way, it also needs to be saved,
481
- since this handler is invoked after instance is saved.
482
- :param request:
483
- :param obj: Model instance that was manipulated, with changes already saved to database
484
- :return: None, this handler should not return anything
485
- """
486
- obj.is_active = True
487
- obj.save()
488
-
489
- def has_submit_line_action_activate_permission(self, request: HttpRequest, object_id: Union[str, int]):
490
- pass
491
-
492
- @action(description=_("Import"), url_path="import")
493
- def changelist_global_action_import(self, request: HttpRequest):
494
- """
495
- Handler for global actions does not receive any queryset or object ids, because it is
496
- meant to be used for general actions for given model.
497
- :param request:
498
- :return: View, as described in section above
499
- """
500
- # This is example of action redirecting to custom page, where the action will be handled
501
- # (with intermediate steps)
502
- return redirect(
503
- reverse_lazy("view-where-import-will-be-handled")
504
- )
505
-
506
- @action(description=_("Row"), url_path="row-action", attrs={"target": "_blank"})
507
- def changelist_row_action_view_on_website(self, request: HttpRequest, object_id: int):
508
- """
509
- Handler for list row action.
510
- :param request:
511
- :param object_id: ID of instance that this action was invoked for
512
- :return: View, as described in section above
513
- """
514
- return redirect(f"https://example.com/{object_id}")
515
-
516
- @action(description=_("Detail"), url_path="detail-action", attrs={"target": "_blank"}, permissions=["change_detail_action_block"])
517
- def change_detail_action_block(self, request: HttpRequest, object_id: int):
518
- """
519
- Handler for detail action.
520
- :param request:
521
- :param object_id: ID of instance that this action was invoked for
522
- :return: View, as described in section above
523
- """
524
- # This is example of action that handled whole logic inside handler
525
- # function and redirects back to object detail
526
- user = User.objects.get(pk=object_id)
527
- user.block()
528
- return redirect(
529
- reverse_lazy("admin:users_user_change", args=(object_id,))
530
- )
531
-
532
-
533
- def has_change_detail_action_block_permission(self, request: HttpRequest, object_id: Union[str, int]):
534
- pass
535
- ```
536
-
537
- ### Action with form example
538
-
539
- Below is an example of an action that will display a form after clicking on the action button on the detail object page.
540
-
541
- ```python
542
- from django import forms
543
- from django.template.loader import render_to_string
544
- from django.urls import reverse_lazy
545
-
546
- from unfold.widgets import UnfoldAdminTextInputWidget
547
-
548
-
549
- class SomeForm(forms.Form):
550
- # It is important to set a widget coming from Unfold
551
- note = forms.CharField(label=_("Note"), widget=UnfoldAdminTextInputWidget)
552
-
553
-
554
- @register(User)
555
- class UserAdmin(ModelAdmin):
556
- actions_detail = ["change_detail_action_block"]
557
-
558
- @action(description=_("Detail"))
559
- def change_detail_action_block(self, request: HttpRequest, object_id: int) -> str:
560
- form = SomeForm(request.POST or None)
561
- user = User.objects.get(pk=object_id)
562
-
563
- if request.method == "POST" and form.is_valid():
564
- # Do something with form data
565
- form.cleaned_data["note"]
566
-
567
- return redirect(
568
- reverse_lazy("admin:users_user_change", args=[object_id])
569
- )
570
-
571
- return render_to_string("some/template.html", {
572
- "form": form,
573
- })
574
- ```
575
-
576
- Template displaying the form. Please note that breadcrumbs are empty in this case but if you want, you can configure your own breadcrumbs path.
577
-
578
- ```html
579
- {% extends "admin/base_site.html" %}
580
-
581
- {% block breadcrumbs %}{% endblock %}
582
-
583
- {% block content %}
584
- <form action="" method="post" novalidate>
585
- {% csrf_token %}
586
-
587
- {% for field in form %}
588
- {% include "unfold/helpers/field.html" with field=field %}
589
- {% endfor %}
590
- </form>
591
- {% endblock %}
592
- ```
593
-
594
- ## Filters
595
-
596
- By 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.
597
-
598
- **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.ModelAdmin` contains boolean `list_filter_submit` flag which enables submit button in filter form.
599
-
600
- ### Text filters
601
-
602
- Text input field which allows filtering by the free string submitted by the user. There are two different variants of this filter: `FieldTextFilter` and `TextFilter`.
603
-
604
- `FieldTextFilter` requires just a model field name and the filter will make `__icontains` search on this field. There are no other things to configure so the integration in `list_filter` will be just one new row looking like `("model_field_name", FieldTextFilter)`.
605
-
606
- In the case of the `TextFilter`, it is needed to write a whole new class inheriting from `TextFilter` with a custom implementation of the `queryset` method and the `parameter_name` attribute. This attribute will be a representation of the search query parameter name in URI. The benefit of the `TextFilter` is the possibility of writing complex queries.
607
-
608
- ```python
609
- from django.contrib import admin
610
- from django.contrib.auth.models import User
611
- from django.core.validators import EMPTY_VALUES
612
- from django.utils.translation import gettext_lazy as _
613
- from unfold.admin import ModelAdmin
614
- from unfold.contrib.filters.admin import TextFilter, FieldTextFilter
615
-
616
- class CustomTextFilter(TextFilter):
617
- title = _("Custom filter")
618
- parameter_name = "query_param_in_uri"
619
-
620
- def queryset(self, request, queryset):
621
- if self.value() not in EMPTY_VALUES:
622
- # Here write custom query
623
- return queryset.filter(your_field=self.value())
624
-
625
- return queryset
626
-
627
-
628
- @admin.register(User)
629
- class MyAdmin(ModelAdmin):
630
- list_filter_submit = True
631
- list_filter = [
632
- ("model_charfield", FieldTextFilter),
633
- CustomTextFilter
634
- ]
635
- ```
636
-
637
- ### Dropdown filters
638
-
639
- Dropdown filters will display a select field with a list of options. Unfold contains two types of dropdowns: `ChoicesDropdownFilter` and `RelatedDropdownFilter`.
640
-
641
- The difference between them is that `ChoicesDropdownFilter` will collect a list of options based on the `choices` attribute of the model field so most commonly it will be used in combination with `CharField` with specified `choices`. On the other hand, `RelatedDropdownFilter` needs a one-to-many or many-to-many foreign key to display options.
642
-
643
- **Note:** At the moment Unfold does not implement a dropdown with an autocomplete functionality, so it is important not to use dropdowns displaying large datasets.
644
-
645
- ```python
646
- # admin.py
647
-
648
- from django.contrib import admin
649
- from django.contrib.auth.models import User
650
- from unfold.admin import ModelAdmin
651
- from unfold.contrib.filters.admin import (
652
- ChoicesDropdownFilter,
653
- MultipleChoicesDropdownFilter,
654
- RelatedDropdownFilter,
655
- MultipleRelatedDropdownFilter,
656
- DropdownFilter,
657
- MultipleDropdownFilter
658
- )
659
-
660
-
661
- class CustomDropdownFilter(DropdownFilter):
662
- title = _("Custom dropdown filter")
663
- parameter_name = "query_param_in_uri"
664
-
665
- def lookups(self, request, model_admin):
666
- return [
667
- ["option_1", _("Option 1")],
668
- ["option_2", _("Option 2")],
669
- ]
670
-
671
- def queryset(self, request, queryset):
672
- if self.value() not in EMPTY_VALUES:
673
- # Here write custom query
674
- return queryset.filter(your_field=self.value())
675
-
676
- return queryset
677
-
678
-
679
- @admin.register(User)
680
- class MyAdmin(ModelAdmin):
681
- list_filter_submit = True
682
- list_filter = [
683
- CustomDropdownFilter,
684
- ("modelfield_with_choices", ChoicesDropdownFilter),
685
- ("modelfield_with_choices_multiple", MultipleChoicesDropdownFilter),
686
- ("modelfield_with_foreign_key", RelatedDropdownFilter)
687
- ("modelfield_with_foreign_key_multiple", MultipleRelatedDropdownFilter)
688
- ]
689
- ```
690
-
691
- ### Numeric filters
692
-
693
- Currently, Unfold implements numeric filters inside `unfold.contrib.filters` application. In order to use these filters, it is required to add this application into `INSTALLED_APPS` in `settings.py` right after `unfold` application.
694
-
695
- ```python
696
- # admin.py
697
-
698
- from django.contrib import admin
699
- from django.contrib.auth.models import User
700
-
701
- from unfold.admin import ModelAdmin
702
- from unfold.contrib.filters.admin import (
703
- RangeNumericListFilter,
704
- RangeNumericFilter,
705
- SingleNumericFilter,
706
- SliderNumericFilter,
707
- )
708
-
709
-
710
- class CustomSliderNumericFilter(SliderNumericFilter):
711
- MAX_DECIMALS = 2
712
- STEP = 10
713
-
714
-
715
- class CustomRangeNumericListFilter(RangeNumericListFilter):
716
- parameter_name = "items_count"
717
- title = "items"
718
-
719
-
720
- @admin.register(User)
721
- class YourModelAdmin(ModelAdmin):
722
- list_filter_submit = True # Submit button at the bottom of the filter
723
- list_filter = (
724
- ("field_A", SingleNumericFilter), # Numeric single field search, __gte lookup
725
- ("field_B", RangeNumericFilter), # Numeric range search, __gte and __lte lookup
726
- ("field_C", SliderNumericFilter), # Numeric range filter but with slider
727
- ("field_D", CustomSliderNumericFilter), # Numeric filter with custom attributes
728
- CustomRangeNumericListFilter, # Numeric range search not restricted to a model field
729
- )
730
-
731
- def get_queryset(self, request):
732
- return super().get_queryset().annotate(items_count=Count("item", distinct=True))
733
- ```
734
-
735
- ### Date/time filters
736
-
737
- ```python
738
- # admin.py
739
-
740
- from django.contrib import admin
741
- from django.contrib.auth.models import User
742
-
743
- from unfold.admin import ModelAdmin
744
- from unfold.contrib.filters.admin import (
745
- RangeDateFilter,
746
- RangeDateTimeFilter,
747
- )
748
-
749
-
750
- @admin.register(User)
751
- class YourModelAdmin(ModelAdmin):
752
- list_filter_submit = True # Submit button at the bottom of the filter
753
- list_filter = (
754
- ("field_E", RangeDateFilter), # Date filter
755
- ("field_F", RangeDateTimeFilter), # Datetime filter
756
- )
757
- ```
758
-
759
- ## Custom admin pages
760
-
761
- By default, Unfold provides a basic view mixin which helps with creation of basic views which are part of Unfold UI. The implementation requires creation of class based view inheriting from `unfold.views.UnfoldModelAdminViewMixin`. It is important to add `title` and `permissions_required` properties.
762
-
763
- ```python
764
- # admin.py
765
-
766
- from django.views.generic import TemplateView
767
- from unfold.admin import ModelAdmin
768
- from unfold.views import UnfoldModelAdminViewMixin
769
-
770
-
771
- class MyClassBasedView(UnfoldModelAdminViewMixin, TemplateView):
772
- title = "Custom Title" # required: custom page header title
773
- permissions_required = () # required: tuple of permissions
774
- template_name = "some/template/path.html"
775
-
776
-
777
- class CustomAdmin(ModelAdmin):
778
- def get_urls(self):
779
- return super().get_urls() + [
780
- path(
781
- "custom-url-path",
782
- MyClassBasedView.as_view(model_admin=self), # IMPORTANT: model_admin is required
783
- name="custom_name"
784
- ),
785
- ]
786
- ```
787
-
788
- The template is straightforward, extend from `unfold/layouts/base.html` and the UI will display all Unfold components like header or sidebar with all menu items. Then all content needs to be located in `content` block.
789
-
790
- ```django-html
791
- {% extends "unfold/layouts/base.html" %}
792
-
793
- {% block content %}
794
- Content here
795
- {% endblock %}
796
- ```
797
-
798
- ## Display decorator
799
-
800
- Unfold introduces it's own `unfold.decorators.display` decorator. By default it has exactly same behavior as native `django.contrib.admin.decorators.display` but it adds same customizations which helps to extends default logic.
801
-
802
- `@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).
803
-
804
- `@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 elements `return "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.
805
-
806
- ```python
807
- # admin.py
808
-
809
- from django.db.models import TextChoices
810
- from django.utils.translation import gettext_lazy as _
811
-
812
- from unfold.admin import ModelAdmin
813
- from unfold.decorators import display
814
-
815
-
816
- class UserStatus(TextChoices):
817
- ACTIVE = "ACTIVE", _("Active")
818
- PENDING = "PENDING", _("Pending")
819
- INACTIVE = "INACTIVE", _("Inactive")
820
- CANCELLED = "CANCELLED", _("Cancelled")
821
-
822
-
823
- class UserAdmin(ModelAdmin):
824
- list_display = [
825
- "display_as_two_line_heading",
826
- "show_status",
827
- "show_status_with_custom_label",
828
- ]
829
-
830
- @display(
831
- description=_("Status"),
832
- ordering="status",
833
- label=True
834
- )
835
- def show_status_default_color(self, obj):
836
- return obj.status
837
-
838
- @display(
839
- description=_("Status"),
840
- ordering="status",
841
- label={
842
- UserStatus.ACTIVE: "success", # green
843
- UserStatus.PENDING: "info", # blue
844
- UserStatus.INACTIVE: "warning", # orange
845
- UserStatus.CANCELLED: "danger", # red
846
- },
847
- )
848
- def show_status_customized_color(self, obj):
849
- return obj.status
850
-
851
- @display(description=_("Status with label"), ordering="status", label=True)
852
- def show_status_with_custom_label(self, obj):
853
- return obj.status, obj.get_status_display()
854
-
855
- @display(header=True)
856
- def display_as_two_line_heading(self, obj):
857
- """
858
- Third argument is short text which will appear as prefix in circle
859
- """
860
- return [
861
- "First main heading",
862
- "Smaller additional description", # Use None in case you don't need it
863
- "AB", # Short text which will appear in front of
864
- # Image instead of initials. Initials are ignored if image is available
865
- {
866
- "path": "some/path/picture.jpg,
867
- "squared": True, # Picture is displayed in square format, if empty circle
868
- "borderless": True # Picture will be displayed without border
869
- "width": 64, # Removes default width. Use together with height
870
- "height": 48, # Removes default height. Use together with width
871
- }
872
- ]
873
- ```
874
-
875
- ## Change form tabs
876
-
877
- When 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 a `tab` CSS class to the fieldset. Once the fieldset contains `tab` class 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.
878
-
879
- ```python
880
- # admin.py
881
-
882
- from django.contrib import admin
883
- from django.utils.translation import gettext_lazy as _
884
- from unfold.admin import ModelAdmin
885
-
886
- from .models import MyModel
887
-
888
-
889
- @admin.register(MyModel)
890
- class MyModelAdmin(ModelAdmin):
891
- fieldsets = (
892
- (
893
- None,
894
- {
895
- "fields": [
896
- "field_1",
897
- "field_2",
898
- ],
899
- },
900
- ),
901
- (
902
- _("Tab 1"),
903
- {
904
- "classes": ["tab"],
905
- "fields": [
906
- "field_3",
907
- "field_4",
908
- ],
909
- },
910
- ),
911
- (
912
- _("Tab 2"),
913
- {
914
- "classes": ["tab"],
915
- "fields": [
916
- "field_5",
917
- "field_6",
918
- ],
919
- },
920
- ),
921
- )
922
- ```
923
-
924
- ## Inlines
925
-
926
- ### Custom title
927
-
928
- By default, the title available for each inline row is coming from the `__str__` implementation of the model. Unfold allows you to override this title by implementing `get_inline_title` on the model which can return your own custom title just for the inline.
929
-
930
- ```python
931
- from unfold.admin import TabularInline
932
-
933
-
934
- class User(models.Model):
935
- # fiels, meta ...
936
-
937
- def get_inline_title(self):
938
- return "Custom title"
939
-
940
-
941
- class MyInline(TabularInline):
942
- model = User
943
- ```
944
-
945
- ### Hide title row
946
-
947
- By applying `hide_title` attribute set to `True`, it is possible to hide the title row which is available for `StackedInline` or `TabularInline`. For `StackedInline` it is required to have disabled delete permission `can_delete` to be able to hide the title row, because the checkbox with the delete action is inside this title.
948
-
949
- ```python
950
- # admin.py
951
-
952
- from unfold.admin import TabularInline
953
-
954
-
955
- class MyInline(TabularInline):
956
- model = User
957
- hide_title = True
958
- ```
959
-
960
- ### Display as tabs
961
-
962
- Inlines can be grouped into tab navigation by specifying `tab` attribute in the inline class.
963
-
964
- ```python
965
- # admin.py
966
-
967
- from unfold.admin import TabularInline
968
-
969
-
970
- class MyInline(TabularInline):
971
- model = User
972
- tab = True
973
- ```
974
-
975
- ### Nonrelated inlines
976
-
977
- To display inlines which are not related (no foreign key pointing at the main model) to the model instance in changeform, you can use nonrelated inlines which are included in `unfold.contrib.inlines` module. Make sure this module is included in `INSTALLED_APPS` in settings.py.
978
-
979
- ```python
980
- from django.contrib.auth.models import User
981
- from unfold.admin import ModelAdmin
982
- from unfold.contrib.inlines.admin import NonrelatedTabularInline
983
- from .models import OtherModel
984
-
985
- class OtherNonrelatedInline(NonrelatedTabularInline): # NonrelatedStackedInline is available as well
986
- model = OtherModel
987
- fields = ["field1", "field2"] # Ignore property to display all fields
988
-
989
- def get_form_queryset(self, obj):
990
- """
991
- Gets all nonrelated objects needed for inlines. Method must be implemented.
992
- """
993
- return self.model.objects.all()
994
-
995
- def save_new_instance(self, parent, instance):
996
- """
997
- Extra save method which can for example update inline instances based on current
998
- main model object. Method must be implemented.
999
- """
1000
- pass
1001
-
1002
-
1003
- @admin.register(User)
1004
- class UserAdmin(ModelAdmin):
1005
- inlines = [OtherNonrelatedInline]
1006
- ```
1007
-
1008
- **NOTE:** credit for this functionality goes to [django-nonrelated-inlines](https://github.com/bhomnick/django-nonrelated-inlines)
1009
-
1010
-
1011
- ## Third party packages
1012
-
1013
- ### django-celery-beat
1014
-
1015
- In general, django-celery-beat does not have any components that require special styling. The default changelist templates are not inheriting from Unfold's `ModelAdmin` but they are using default `ModelAdmin` coming from `django.contrib.admin` which is causing some design discrepancies in the changelist.
1016
-
1017
- In the source code below you can find a short code snippet to unregister all `django-celery-beat` admin classes and register them with the proper parent `ModelAdmin` class.
1018
-
1019
- ```python
1020
- # admin.py
1021
- from django.contrib import admin
1022
- from unfold.admin import ModelAdmin
1023
-
1024
- from django_celery_beat.models import (
1025
- ClockedSchedule,
1026
- CrontabSchedule,
1027
- IntervalSchedule,
1028
- PeriodicTask,
1029
- SolarSchedule,
1030
- )
1031
- from django_celery_beat.admin import ClockedScheduleAdmin as BaseClockedScheduleAdmin
1032
- from django_celery_beat.admin import CrontabScheduleAdmin as BaseCrontabScheduleAdmin
1033
- from django_celery_beat.admin import PeriodicTaskAdmin as BasePeriodicTaskAdmin
1034
- from django_celery_beat.admin import PeriodicTaskForm, TaskSelectWidget
1035
-
1036
- admin.site.unregister(PeriodicTask)
1037
- admin.site.unregister(IntervalSchedule)
1038
- admin.site.unregister(CrontabSchedule)
1039
- admin.site.unregister(SolarSchedule)
1040
- admin.site.unregister(ClockedSchedule)
1041
-
1042
-
1043
- class UnfoldTaskSelectWidget(UnfoldAdminSelectWidget, TaskSelectWidget):
1044
- pass
1045
-
1046
-
1047
- class UnfoldPeriodicTaskForm(PeriodicTaskForm):
1048
- def __init__(self, *args, **kwargs):
1049
- super().__init__(*args, **kwargs)
1050
- self.fields["task"].widget = UnfoldAdminTextInputWidget()
1051
- self.fields["regtask"].widget = UnfoldTaskSelectWidget()
1052
-
1053
-
1054
- @admin.register(PeriodicTask)
1055
- class PeriodicTaskAdmin(BasePeriodicTaskAdmin, ModelAdmin):
1056
- form = UnfoldPeriodicTaskForm
1057
-
1058
-
1059
- @admin.register(IntervalSchedule)
1060
- class IntervalScheduleAdmin(ModelAdmin):
1061
- pass
1062
-
1063
-
1064
- @admin.register(CrontabSchedule)
1065
- class CrontabScheduleAdmin(BaseCrontabScheduleAdmin, ModelAdmin):
1066
- pass
1067
-
1068
-
1069
- @admin.register(SolarSchedule)
1070
- class SolarScheduleAdmin(ModelAdmin):
1071
- pass
1072
-
1073
- @admin.register(ClockedSchedule)
1074
- class ClockedScheduleAdmin(BaseClockedScheduleAdmin, ModelAdmin):
1075
- pass
1076
- ```
1077
-
1078
- ### django-guardian
1079
-
1080
- Adding support for django-guardian is quite straightforward in Unfold, just add `unfold.contrib.guardian` to `INSTALLED_APPS` at the beginning of the file. This action will override all templates coming from the django-guardian. Please note that **Object permissions** link is available in top right dropdown navigation.
1081
-
1082
- ### django-import-export
1083
-
1084
- 1. Add `unfold.contrib.import_export` to `INSTALLED_APPS` at the beginning of the file. This action will override all templates coming from the application.
1085
- 2. Change `import_form_class` and `export_form_class` in ModelAdmin which is inheriting from `ImportExportModelAdmin`. This chunk of code is responsible for adding proper styling to form elements.
1086
-
1087
- ```python
1088
- # admin.py
1089
-
1090
- from unfold.admin import ModelAdmin
1091
- from import_export.admin import ImportExportModelAdmin
1092
- from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm
1093
-
1094
- class ExampleAdmin(ModelAdmin, ImportExportModelAdmin):
1095
- import_form_class = ImportForm
1096
- export_form_class = ExportForm
1097
- # export_form_class = SelectableFieldsExportForm
1098
- ```
1099
-
1100
- When implementing `import_export.admin.ExportActionModelAdmin` class 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.admin` contains class with the same name `ExportActionModelAdmin` which inherits behavior of parent form and adds appropriate CSS classes.
1101
-
1102
- **Note:** This class has been removed and in new version (4.x) of django-import-export it is not needed.
1103
-
1104
- ```python
1105
- admin.py
1106
-
1107
- from unfold.admin import ModelAdmin
1108
- from unfold.contrib.import_export.admin import ExportActionModelAdmin
1109
-
1110
- class ExampleAdmin(ModelAdmin, ExportActionModelAdmin):
1111
- pass
1112
- ```
1113
-
1114
- ### django-modeltranslation
1115
-
1116
- By default, Unfold supports django-modeltranslation and `TabbedTranslationAdmin` admin class for the tabbed navigation is implemented with custom styling as well.
1117
-
1118
- ```python
1119
- from django.contrib import admin
1120
-
1121
- from modeltranslation.admin import TabbedTranslationAdmin
1122
- from unfold.admin import ModelAdmin
1123
-
1124
- from .models import MyModel
1125
-
1126
-
1127
- @admin.register(MyModel)
1128
- class MyModelAdmin(ModelAdmin, TabbedTranslationAdmin):
1129
- pass
1130
- ```
1131
-
1132
- For 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.
1133
-
1134
- ```python
1135
- # settings.py
1136
-
1137
- UNFOLD = {
1138
- "EXTENSIONS": {
1139
- "modeltranslation": {
1140
- "flags": {
1141
- "en": "🇬🇧",
1142
- "fr": "🇫🇷",
1143
- "nl": "🇧🇪",
1144
- },
1145
- },
1146
- },
1147
- }
1148
- ```
1149
-
1150
- ### django-money
1151
-
1152
- This application is supported in Unfold by default. It is not needed to add any other applications into `INSTALLED_APPS`. Unfold is recognizing special form widget coming from django-money and applying specific styling.
1153
-
1154
- ### django-simple-history
1155
-
1156
- To make this application work, add `unfold.contrib.simple_history` into `settings.py` in `INSTALLED_APPS` variable before right after `unfold`. This app should ensure that templates coming from django-simple-history are overridden by Unfold.
1157
-
1158
- ## User Admin Form
1159
-
1160
- User'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.
1161
-
1162
- ```python
1163
- # models.py
1164
-
1165
- from django.contrib.admin import register
1166
- from django.contrib.auth.models import User
1167
- from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
1168
-
1169
- from unfold.admin import ModelAdmin
1170
- from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm
1171
-
1172
-
1173
- @register(User)
1174
- class UserAdmin(BaseUserAdmin, ModelAdmin):
1175
- form = UserChangeForm
1176
- add_form = UserCreationForm
1177
- change_password_form = AdminPasswordChangeForm
1178
- ```
1179
-
1180
- ## Adding custom styles and scripts
1181
-
1182
- To add new custom styles, for example for custom dashboard, it is possible to load them via **STYLES** key in **UNFOLD** dict. 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, but **SCRIPTS** is used.
1183
-
1184
- ```python
1185
- # settings.py
1186
-
1187
- from django.templatetags.static import static
1188
-
1189
- UNFOLD = {
1190
- "STYLES": [
1191
- lambda request: static("css/style.css"),
1192
- ],
1193
- "SCRIPTS": [
1194
- lambda request: static("js/script.js"),
1195
- ],
1196
- }
1197
- ```
1198
-
1199
- ## Project level Tailwind stylesheet
1200
-
1201
- When 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.
1202
-
1203
- ```javascript
1204
- // tailwind.config.js
1205
-
1206
- module.exports = {
1207
- content: ["./your_project/**/*.{html,py,js}"],
1208
- // In case custom colors are defined in UNFOLD["COLORS"]
1209
- colors: {
1210
- primary: {
1211
- 50: "rgb(var(--color-primary-50) / <alpha-value>)",
1212
- 100: "rgb(var(--color-primary-100) / <alpha-value>)",
1213
- 200: "rgb(var(--color-primary-200) / <alpha-value>)",
1214
- 300: "rgb(var(--color-primary-300) / <alpha-value>)",
1215
- 400: "rgb(var(--color-primary-400) / <alpha-value>)",
1216
- 500: "rgb(var(--color-primary-500) / <alpha-value>)",
1217
- 600: "rgb(var(--color-primary-600) / <alpha-value>)",
1218
- 700: "rgb(var(--color-primary-700) / <alpha-value>)",
1219
- 800: "rgb(var(--color-primary-800) / <alpha-value>)",
1220
- 900: "rgb(var(--color-primary-900) / <alpha-value>)",
1221
- 950: "rgb(var(--color-primary-950) / <alpha-value>)",
1222
- },
1223
- },
1224
- };
1225
- ```
1226
-
1227
- Once the configuration file is set, it is possible to compile new styles which can be loaded into admin by using **STYLES** key in **UNFOLD** dict.
1228
-
1229
- ```bash
1230
- npx tailwindcss -o your_project/static/css/styles.css --watch --minify
1231
- ```
1232
-
1233
- ## Admin dashboard
1234
-
1235
- ### Overriding template
1236
-
1237
- Create `templates/admin/index.html` in 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 required instructions are located in [Project Level Tailwind Stylesheet](#project-level-tailwind-stylesheet) chapter.
1238
-
1239
- ```html+django
1240
- {% extends 'unfold/layouts/base_simple.html' %}
1241
-
1242
- {% load cache humanize i18n %}
1243
-
1244
- {% block breadcrumbs %}{% endblock %}
1245
-
1246
- {% block title %}
1247
- {% if subtitle %}
1248
- {{ subtitle }} |
1249
- {% endif %}
1250
-
1251
- {{ title }} | {{ site_title|default:_('Django site admin') }}
1252
- {% endblock %}
1253
-
1254
- {% block branding %}
1255
- <h1 id="site-name">
1256
- <a href="{% url 'admin:index' %}">
1257
- {{ site_header|default:_('Django administration') }}
1258
- </a>
1259
- </h1>
1260
- {% endblock %}
1261
-
1262
- {% block content %}
1263
- Start creating your own Tailwind components here
1264
- {% endblock %}
1265
- ```
1266
-
1267
- ### Custom variables
1268
-
1269
- When 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 special `DASHBOARD_CALLBACK` parameter which allows passing a dictionary of variables to `templates/admin/index.html` template.
1270
-
1271
- ```python
1272
- # views.py
1273
-
1274
- def dashboard_callback(request, context):
1275
- context.update({
1276
- "custom_variable": "value",
1277
- })
1278
-
1279
- return context
1280
- ```
1281
-
1282
- ```python
1283
- # settings.py
1284
-
1285
- UNFOLD = {
1286
- "DASHBOARD_CALLBACK": "app.views.dashboard_callback",
1287
- }
1288
- ```
1289
-
1290
- ### Unfold components
1291
-
1292
- Unfold 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.
1293
-
1294
- 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 includes `children` variable which contains a value specified in the parent component. Except for `children` variable, 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.
1295
-
1296
- ```html+django
1297
- {% component "unfold/components/flex.html" with col=1 %}
1298
- {% component "unfold/components/card.html" %}
1299
- {% component "unfold/components/title.html" %}
1300
- Card Title
1301
- {% endcomponent %}
1302
- {% endcomponent %}
1303
- {% endcomponent %}
1304
- ```
1305
-
1306
- Below you can find a more complex example which is using multiple components and processing them based on the passed variables from the `DASHBOARD_CALLBACK`.
1307
-
1308
- ```html+django
1309
- {% load i18n %}
1310
-
1311
- {% block content %}
1312
- {% component "unfold/components/container.html" %}
1313
- {% component "unfold/components/flex.html" with class="gap-4"%}
1314
- {% component "unfold/components/navigation.html" with items=navigation %}
1315
- {% endcomponent %}
1316
-
1317
- {% component "unfold/components/navigation.html" with class="ml-auto" items=filters %}
1318
- {% endcomponent %}
1319
- {% endcomponent %}
1320
-
1321
- {% component "unfold/components/flex.html" with class="gap-8 mb-8 flex-col lg:flex-row" %}
1322
- {% for card in cards %}
1323
- {% trans "Last 7 days" as label %}
1324
- {% component "unfold/components/card.html" with class="lg:w-1/3" %}
1325
- {% component "unfold/components/text.html" %}
1326
- {{ card.title }}
1327
- {% endcomponent %}
1328
-
1329
- {% component "unfold/components/title.html" %}
1330
- {{ card.metric }}
1331
- {% endcomponent %}
1332
- {% endcomponent %}
1333
- {% endfor %}
1334
- {% endcomponent %}
1335
- {% endcomponent %}
1336
- {% endblock %}
1337
- ```
1338
-
1339
- #### List of available components <!-- omit from toc -->
1340
-
1341
- | Component | Description | Arguments |
1342
- | --------------------------------- | ------------------------------ | ------------------------------------ |
1343
- | unfold/components/button.html | Basic button element | submit |
1344
- | unfold/components/card.html | Card component | class, title, footer, label, icon |
1345
- | unfold/components/chart/bar.html | Bar chart implementation | class, data, height, width |
1346
- | unfold/components/chart/line.html | Line chart implementation | class, data, height, width |
1347
- | unfold/components/container.html | Wrapper for settings max width | class |
1348
- | unfold/components/flex.html | Flex items | class, col |
1349
- | unfold/components/icon.html | Icon element | class |
1350
- | unfold/components/navigation.html | List of navigation links | class, items |
1351
- | unfold/components/progress.html | Percentual progress bar | class, value, title, description |
1352
- | unfold/components/separator.html | Separator, horizontal rule | class |
1353
- | unfold/components/table.html | Table | table, card_included, striped |
1354
- | unfold/components/text.html | Paragraph of text | class |
1355
- | unfold/components/title.html | Basic heading element | class |
1356
-
1357
-
1358
- #### Table component example
1359
-
1360
- ```python
1361
- from typing import Dict
1362
- from django.http import HttpRequest
1363
-
1364
-
1365
- def dashboard_callback(request: HttpRequest) -> Dict:
1366
- return {
1367
- "table_data": {
1368
- "headers": ["col 1", "col 2"],
1369
- "rows": [
1370
- ["a", "b"],
1371
- ["c", "d"],
1372
- ]
1373
- }
1374
- }
1375
- ```
1376
-
1377
- ```django-html
1378
- {% component "unfold/components/card.html" with title="Card title" %}
1379
- {% component "unfold/components/table.html" with table=table_data card_included=1 striped=1 %}{% endcomponent %}
1380
- {% endcomponent %}
1381
- ```
1382
-
1383
- ## Unfold development
1384
-
1385
- ### Pre-commit
1386
-
1387
- Before adding any source code, it is recommended to have pre-commit installed on your local computer to check for all potential issues when committing the code.
1388
-
1389
- ```bash
1390
- pip install pre-commit
1391
- pre-commit install
1392
- pre-commit install --hook-type commit-msg
1393
- pre-commit run --all-files # Check if everything is okay
1394
- ```
1395
-
1396
- ### Poetry configuration
1397
-
1398
- To add a new feature or fix the easiest approach is to use django-unfold in combination with Poetry. The process looks like:
1399
-
1400
- - Install django-unfold via `poetry add django-unfold`
1401
- - After that it is needed to git clone the repository somewhere on local computer.
1402
- - Edit _pyproject.toml_ and update django-unfold line `django-unfold = { path = "../django-unfold", develop = true}`
1403
- - Lock and update via `poetry lock && poetry update`
1404
-
1405
- ### Compiling Tailwind
1406
-
1407
- At 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 classes occurrences.
1408
-
1409
- ```bash
1410
- npm install
1411
- npx tailwindcss -i src/unfold/styles.css -o src/unfold/static/unfold/css/styles.css --watch --minify
1412
-
1413
- npm run tailwind:watch # run after each change in code
1414
- npm run tailwind:build # run once
1415
- ```
1416
-
1417
- Some components like datepickers, calendars or selectors in admin was not possible to style by overriding html templates so their default styles are overridden in **styles.css**.
1418
-
1419
- **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.
1420
-
1421
-
1422
- ### Design system
1423
-
1424
- | Component | Classes |
1425
- | --------------------------------- | ------------------------------------------------------ |
1426
- | Regular text | text-gray-600 dark:text-gray-300 |
1427
- | Hover regular text | text-gray-700 dark:text-gray-200 |
1428
- | Headings | font-semibold text-gray-900 dark:text-gray-100 |
1429
- | Icon | text-gray-400 dark:text-gray-500 |
1430
- | Hover icon | hover:text-gray-500 dark:hover:text-gray-400 |
1431
-
1432
- ### Using VS Code with containers
1433
-
1434
- Unfold already contains prepared support for VS Code development. After cloning the project locally, open the main folder in VS Code (in terminal `code .`). Immediately, you would see a message from VS Code **Folder contains a Dev Container configuration file. Reopen folder to develop in a container** which will inform you that the support for containers is prepared. Confirm the message by clicking on **Reopen in Container**. If the message is not there, you can still manually open the project in a container by running the command **Dev Containers: Reopen in Container**.
1435
-
1436
- #### Development server
1437
-
1438
- Now the VS Code will build an image and install Python dependencies. After successful installation is completed, VS Code will spin a container and from now it is possible to directly develop in the container. Unfold contains an example development application with the basic Unfold configuration available under `tests/server`. Run `python manage.py runserver` within a `tests/server` folder to start a development Django server. Note that you have to run the command from VS Code terminal which is already connected to the running container.
1439
-
1440
- **Note:** this is not a production ready server. Use it just for running tests or developing features & fixes.
1441
-
1442
- #### Compiling Tailwind in devcontainer
1443
-
1444
- The container has already a node preinstalled so it is possible to compile a new CSS. Open the terminal and run `npm install` which will install all dependencies and will create `node_modules` folder. Now, you can run npm commands for Tailwind as described in the previous chapter.
1445
-
1446
- ## Credits
1447
-
1448
- - [django-nonrelated-inlines](https://github.com/bhomnick/django-nonrelated-inlines) - Django admin inlines for unrelated models
1449
- - [TailwindCSS](https://tailwindcss.com/) - CSS framework
1450
- - [HTMX](https://htmx.org/) - AJAX communication with backend
1451
- - [Material Icons](https://fonts.google.com/icons) - Icons from Google Fonts
1452
- - [Trix](https://trix-editor.org/) - WYSIWYG editor
1453
- - [Alpine.js](https://alpinejs.dev/) - JavaScript interactions
1454
- - [Chart.js](https://github.com/chartjs/Chart.js/) - Chart components
1455
-