django-unfold 0.23.0__py3-none-any.whl → 0.24.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_unfold-0.23.0.dist-info → django_unfold-0.24.0.dist-info}/METADATA +97 -4
- {django_unfold-0.23.0.dist-info → django_unfold-0.24.0.dist-info}/RECORD +20 -19
- unfold/admin.py +3 -3
- unfold/contrib/filters/admin.py +116 -0
- unfold/contrib/filters/forms.py +29 -1
- unfold/contrib/filters/templates/unfold/filters/filters_date_range.html +1 -1
- unfold/contrib/filters/templates/unfold/filters/filters_datetime_range.html +1 -1
- unfold/contrib/filters/templates/unfold/filters/filters_field.html +7 -0
- unfold/contrib/filters/templates/unfold/filters/filters_numeric_range.html +1 -1
- unfold/contrib/filters/templates/unfold/filters/filters_numeric_single.html +1 -1
- unfold/contrib/filters/templates/unfold/filters/filters_numeric_slider.html +1 -1
- unfold/contrib/import_export/admin.py +1 -1
- unfold/contrib/import_export/forms.py +6 -7
- unfold/contrib/import_export/templates/admin/import_export/export.html +1 -1
- unfold/contrib/import_export/templates/admin/import_export/import_form.html +2 -2
- unfold/templates/admin/filter.html +2 -2
- unfold/templates/unfold/helpers/display_header.html +16 -13
- unfold/widgets.py +2 -2
- {django_unfold-0.23.0.dist-info → django_unfold-0.24.0.dist-info}/LICENSE.md +0 -0
- {django_unfold-0.23.0.dist-info → django_unfold-0.24.0.dist-info}/WHEEL +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: django-unfold
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.24.0
|
4
4
|
Summary: Modern Django admin theme for seamless interface development
|
5
5
|
Home-page: https://unfoldadmin.com
|
6
6
|
License: MIT
|
@@ -52,14 +52,14 @@ Did you decide to start using Unfold but you don't have time to make the switch
|
|
52
52
|
- **Dependencies:** completely based only on `django.contrib.admin`
|
53
53
|
- **Actions:** multiple ways how to define actions within different parts of admin
|
54
54
|
- **WYSIWYG:** built-in support for WYSIWYG (Trix)
|
55
|
-
- **
|
55
|
+
- **Filters:** custom dropdown, numeric, datetime, and text fields
|
56
56
|
- **Dashboard:** custom components for rapid dashboard development
|
57
57
|
- **Model tabs:** define custom tab navigations for models
|
58
58
|
- **Fieldset tabs:** merge several fielsets into tabs in change form
|
59
59
|
- **Colors:** possibility to override default color scheme
|
60
60
|
- **Third party packages:** default support for multiple popular applications
|
61
61
|
- **Environment label**: distinguish between environments by displaying a label
|
62
|
-
- **Parallel admin**: support for having default admin in parallel with Unfold
|
62
|
+
- **Parallel admin**: support for having default admin in parallel with Unfold. [Admin migration guide](https://unfoldadmin.com/blog/migrating-django-admin-unfold/)
|
63
63
|
- **VS Code**: project configuration and development container is included
|
64
64
|
|
65
65
|
## Table of contents <!-- omit from toc -->
|
@@ -74,6 +74,8 @@ Did you decide to start using Unfold but you don't have time to make the switch
|
|
74
74
|
- [Action handler functions](#action-handler-functions)
|
75
75
|
- [Action examples](#action-examples)
|
76
76
|
- [Filters](#filters)
|
77
|
+
- [Text filters](#text-filters)
|
78
|
+
- [Dropdown filters](#dropdown-filters)
|
77
79
|
- [Numeric filters](#numeric-filters)
|
78
80
|
- [Date/time filters](#datetime-filters)
|
79
81
|
- [Display decorator](#display-decorator)
|
@@ -481,6 +483,88 @@ By default, Django admin handles all filters as regular HTML links pointing at t
|
|
481
483
|
|
482
484
|
**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.
|
483
485
|
|
486
|
+
### Text filters
|
487
|
+
|
488
|
+
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`.
|
489
|
+
|
490
|
+
`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)`.
|
491
|
+
|
492
|
+
In the case of the `TextFilter`, it is needed the 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.
|
493
|
+
|
494
|
+
```python
|
495
|
+
from django.contrib import admin
|
496
|
+
from django.contrib.auth.models import User
|
497
|
+
from django.core.validators import EMPTY_VALUES
|
498
|
+
from django.utils.translation import gettext_lazy as _
|
499
|
+
from unfold.admin import ModelAdmin
|
500
|
+
from unfold.contrib.filters.admin import TextFilter, FieldTextFilter
|
501
|
+
|
502
|
+
class CustomTextFilter(TextFilter):
|
503
|
+
title = _("Custom filter")
|
504
|
+
parameter_name = "query_param_in_uri"
|
505
|
+
|
506
|
+
def queryset(self, request, queryset):
|
507
|
+
if self.value() not in EMPTY_VALUES:
|
508
|
+
# Here write custom query
|
509
|
+
return queryset.filter(your_field=self.value())
|
510
|
+
|
511
|
+
return queryset
|
512
|
+
|
513
|
+
|
514
|
+
@admin.register(User)
|
515
|
+
class MyAdmin(ModelAdmin):
|
516
|
+
list_filter_submit = True
|
517
|
+
list_filter = [
|
518
|
+
("model_charfield", FieldTextFilter),
|
519
|
+
CustomTextFilter
|
520
|
+
]
|
521
|
+
```
|
522
|
+
|
523
|
+
### Dropdown filters
|
524
|
+
|
525
|
+
Dropdown filters will display a select field with a list of options. Unfold contains two types of dropdowns: `ChoicesDropdownFilter` and `RelatedDropdownFilter`.
|
526
|
+
|
527
|
+
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 side, `RelatedDropdownFilter` needs a one-to-many or many-to-many foreign key to display options.
|
528
|
+
|
529
|
+
**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.
|
530
|
+
|
531
|
+
```python
|
532
|
+
# admin.py
|
533
|
+
|
534
|
+
from django.contrib import admin
|
535
|
+
from django.contrib.auth.models import User
|
536
|
+
from unfold.admin import ModelAdmin
|
537
|
+
from unfold.contrib.filters.admin import ChoicesDropdownFilter, RelatedDropdownFilter, DropdownFilter
|
538
|
+
|
539
|
+
|
540
|
+
class CustomDropdownFilter(DropdownFilter):
|
541
|
+
title = _("Custom dropdown filter")
|
542
|
+
parameter_name = "query_param_in_uri"
|
543
|
+
|
544
|
+
def lookups(self, request, model_admin):
|
545
|
+
return [
|
546
|
+
["option_1", _("Option 1")],
|
547
|
+
["option_2", _("Option 2")],
|
548
|
+
]
|
549
|
+
|
550
|
+
def queryset(self, request, queryset):
|
551
|
+
if self.value() not in EMPTY_VALUES:
|
552
|
+
# Here write custom query
|
553
|
+
return queryset.filter(your_field=self.value())
|
554
|
+
|
555
|
+
return queryset
|
556
|
+
|
557
|
+
|
558
|
+
@admin.register(User)
|
559
|
+
class MyAdmin(ModelAdmin):
|
560
|
+
list_filter_submit = True
|
561
|
+
list_filter = [
|
562
|
+
CustomDropdownFilter,
|
563
|
+
("modelfield_with_choices", ChoicesDropdownFilter),
|
564
|
+
("modelfield_with_foreign_key", RelatedDropdownFilter)
|
565
|
+
]
|
566
|
+
```
|
567
|
+
|
484
568
|
### Numeric filters
|
485
569
|
|
486
570
|
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.
|
@@ -611,7 +695,16 @@ class UserAdmin(ModelAdmin):
|
|
611
695
|
"""
|
612
696
|
Third argument is short text which will appear as prefix in circle
|
613
697
|
"""
|
614
|
-
return
|
698
|
+
return [
|
699
|
+
"First main heading",
|
700
|
+
"Smaller additional description", # Use None in case you don't need it
|
701
|
+
"AB", # Short text which will appear in front of
|
702
|
+
# Image instead of initials. Initials are ignored if image is available
|
703
|
+
{
|
704
|
+
"path": "some/path/picture.jpg,
|
705
|
+
"squared": True, # Picture is displayed in square format, if empty circle
|
706
|
+
}
|
707
|
+
]
|
615
708
|
```
|
616
709
|
|
617
710
|
## Change form tabs
|
@@ -1,22 +1,23 @@
|
|
1
1
|
unfold/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
unfold/admin.py,sha256=
|
2
|
+
unfold/admin.py,sha256=iNlDiZy_kl4lk2We4VDKOK5lMam4GeNMOA-bo67ykqU,23810
|
3
3
|
unfold/apps.py,sha256=SlBXPYrUd2uXn67qFbRvbXSUk3XFWrF4-5WELgDCvho,381
|
4
4
|
unfold/checks.py,sha256=Smgji9w19hnYjJElJ_FJnnyTEAE-E-OUB6otHu7lasY,1670
|
5
5
|
unfold/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
6
|
unfold/contrib/filters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
-
unfold/contrib/filters/admin.py,sha256=
|
7
|
+
unfold/contrib/filters/admin.py,sha256=hkrw-CthPsSubwsDpInqOiNw5vsl_zbQeYW23DnKSsY,20068
|
8
8
|
unfold/contrib/filters/apps.py,sha256=wEySJy0gMLzFLb9XNKE-RexiO05X7NaQ5QmxZyziJ_k,136
|
9
|
-
unfold/contrib/filters/forms.py,sha256
|
9
|
+
unfold/contrib/filters/forms.py,sha256=cdBFNB45PPgKVzAbjrwbZVdYF6rZBUQPxxWMwZaKCpM,4736
|
10
10
|
unfold/contrib/filters/static/unfold/filters/css/nouislider.min.css,sha256=rddL_jOGGVEY6wR-aw0VYovAfz5fPeAIsulrlSNb1hc,4221
|
11
11
|
unfold/contrib/filters/static/unfold/filters/js/DateTimeShortcuts.js,sha256=jgFNBDf6aHvUlyv0LEDHggXO-xA8pWOCWFWcVupdA30,19332
|
12
12
|
unfold/contrib/filters/static/unfold/filters/js/admin-numeric-filter.js,sha256=nTkiiJk4Abn9d6KigxPSEsereFhFq-v5n_boiiY1eII,1668
|
13
13
|
unfold/contrib/filters/static/unfold/filters/js/nouislider.min.js,sha256=aIEt5UlLNnEv4-HPyxcLqu9dTS7gXiMm35Mm0rFmQ0Q,26683
|
14
14
|
unfold/contrib/filters/static/unfold/filters/js/wNumb.min.js,sha256=gayD3el5iizhy0DlsyKfZvfOfOZHDmb7BPvOcT97GSg,2236
|
15
|
-
unfold/contrib/filters/templates/unfold/filters/filters_date_range.html,sha256=
|
16
|
-
unfold/contrib/filters/templates/unfold/filters/filters_datetime_range.html,sha256=
|
17
|
-
unfold/contrib/filters/templates/unfold/filters/
|
18
|
-
unfold/contrib/filters/templates/unfold/filters/
|
19
|
-
unfold/contrib/filters/templates/unfold/filters/
|
15
|
+
unfold/contrib/filters/templates/unfold/filters/filters_date_range.html,sha256=BVUsF4vCtDpxpXxevf--y3n8kO1FH3FMFbLG1qEUe6g,661
|
16
|
+
unfold/contrib/filters/templates/unfold/filters/filters_datetime_range.html,sha256=BVUsF4vCtDpxpXxevf--y3n8kO1FH3FMFbLG1qEUe6g,661
|
17
|
+
unfold/contrib/filters/templates/unfold/filters/filters_field.html,sha256=UTlSZlpg-gAc_a-EJLLF0NI_ofuSHQ2kMMoAs99nL2E,164
|
18
|
+
unfold/contrib/filters/templates/unfold/filters/filters_numeric_range.html,sha256=NoJwm36x1J65Pq8cLk_g_qJ0Cil3CtDwja9dqZgRX8g,639
|
19
|
+
unfold/contrib/filters/templates/unfold/filters/filters_numeric_single.html,sha256=EeQv2fHYX6MK9wwM0lgGkKGfmyDo82VLtx_E0M9MUtI,576
|
20
|
+
unfold/contrib/filters/templates/unfold/filters/filters_numeric_slider.html,sha256=SpkLgq_m1-7WSdZzf3IPcySXxdaqe0z6qljAhhZSHec,1681
|
20
21
|
unfold/contrib/forms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
21
22
|
unfold/contrib/forms/apps.py,sha256=Di0TMzVuRpVxLG-8Bjdq5ALCSf5r7u2xVhD0jU6H5Sc,132
|
22
23
|
unfold/contrib/forms/static/unfold/forms/css/trix.css,sha256=TH9WdnaZrmwI8hAEydwjobdrBzSw_KYdRTSQDuD-8hE,20027
|
@@ -35,18 +36,18 @@ unfold/contrib/guardian/templates/admin/guardian/model/obj_perms_manage_user.htm
|
|
35
36
|
unfold/contrib/guardian/templates/unfold/guardian/group_form.html,sha256=P8WMC5EejUHV5AxEiIQ2LOGzefLHk5J5UHiNq9wnBgY,4145
|
36
37
|
unfold/contrib/guardian/templates/unfold/guardian/user_form.html,sha256=ci7FRrhTEKbFKKxsJ-07_dWXBYz4mqXPoqu5HfqYLaM,4132
|
37
38
|
unfold/contrib/import_export/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
38
|
-
unfold/contrib/import_export/admin.py,sha256=
|
39
|
+
unfold/contrib/import_export/admin.py,sha256=h6CKRuvloEdxcVScycTSAShXEfzEAsL75uMj2ullhOM,1151
|
39
40
|
unfold/contrib/import_export/apps.py,sha256=SdJu6Qh90VqGWY19FSDhhpUqhTbaIYsJKny3zX5baHI,149
|
40
|
-
unfold/contrib/import_export/forms.py,sha256=
|
41
|
+
unfold/contrib/import_export/forms.py,sha256=DCTjCqeWT0jyPAMZgDDad0LoWtKRGu1Q5TohNpnpMAA,637
|
41
42
|
unfold/contrib/import_export/templates/admin/import_export/base.html,sha256=loL2qcV-f8aAzkHss_I4IkwfgemVW2CjOu_aiBxdwX0,357
|
42
43
|
unfold/contrib/import_export/templates/admin/import_export/change_list_export_item.html,sha256=pTDeqPKOlCPKH2dxMIfPnWuc2wVDzB7AzL73WbxSnRY,257
|
43
44
|
unfold/contrib/import_export/templates/admin/import_export/change_list_import_export.html,sha256=JdKd6P2Ot9Ou4yg4CywTauuE1UiTz_mRvDwlx3vj3LI,229
|
44
45
|
unfold/contrib/import_export/templates/admin/import_export/change_list_import_item.html,sha256=XUuRxnsx9YQbKvW-E_JGl_ha7kpTSGSoRefOTTizuX0,233
|
45
|
-
unfold/contrib/import_export/templates/admin/import_export/export.html,sha256=
|
46
|
+
unfold/contrib/import_export/templates/admin/import_export/export.html,sha256=W-ZId8LJCzA3Kuzxun7v-RNzqo7q8cxRk-vmd8xaC0s,1786
|
46
47
|
unfold/contrib/import_export/templates/admin/import_export/import.html,sha256=P54_f3s96PV87Bo-FCZfmsn9DkRXLOB36r7HYF6y7GM,2075
|
47
48
|
unfold/contrib/import_export/templates/admin/import_export/import_confirm.html,sha256=M-acK4XSLHuPFD_NJashGYvPPeJrJsC-3LMvHs3lRis,867
|
48
49
|
unfold/contrib/import_export/templates/admin/import_export/import_errors.html,sha256=0DmJvZs31u-E2Y53yySci86cTnG9aUnOzvfYrOo0lYA,1422
|
49
|
-
unfold/contrib/import_export/templates/admin/import_export/import_form.html,sha256=
|
50
|
+
unfold/contrib/import_export/templates/admin/import_export/import_form.html,sha256=F0cLh5AAyVpgxPClKcIXSt1bLRR1lESTdaSMU0Z_6G4,1303
|
50
51
|
unfold/contrib/import_export/templates/admin/import_export/import_preview.html,sha256=pNuLDW6zc5yOF1jurL2EgR0j05RL9ZVJLZiV4R21GJc,2413
|
51
52
|
unfold/contrib/import_export/templates/admin/import_export/import_validation.html,sha256=1wQOiXN_Ga9VO6GGyl__KEiuJlCh4gTqzZdzIbmKxG0,4880
|
52
53
|
unfold/contrib/simple_history/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -92,7 +93,7 @@ unfold/templates/admin/delete_confirmation.html,sha256=hpa2E14oZEXBBs6W1qdNQuF65
|
|
92
93
|
unfold/templates/admin/delete_selected_confirmation.html,sha256=Foka2yvwAMEZre-Kh1KNadRzrCotdKM2U4e6AJQYZu8,4941
|
93
94
|
unfold/templates/admin/edit_inline/stacked.html,sha256=U6O_r9-5Hp7mT0l1EgTeBB8tc9nEAAi0etL2aCU2pBM,4415
|
94
95
|
unfold/templates/admin/edit_inline/tabular.html,sha256=YOrCgAQ9apLZf8VtFmUhK-WFoQomPtlilT9ktkEFBQA,12315
|
95
|
-
unfold/templates/admin/filter.html,sha256=
|
96
|
+
unfold/templates/admin/filter.html,sha256=dkrFkei-EAlldIU8DrgvSChzWQuUOu6-LS_qlZxdfFw,1708
|
96
97
|
unfold/templates/admin/includes/fieldset.html,sha256=r4XjcZAOkWxHQExHZBWoCGtO3LYL0Iwkw1C55oR5V6M,2898
|
97
98
|
unfold/templates/admin/includes/object_delete_summary.html,sha256=Nv69SCzyJHFX14iJFfodxKM0IIpQegKZH0fvKB15QJI,468
|
98
99
|
unfold/templates/admin/index.html,sha256=pkGdKWdD3zzOvkRdELvdb15sleSpfl4eHPA14PAh7z0,684
|
@@ -124,7 +125,7 @@ unfold/templates/unfold/helpers/app_list.html,sha256=FvL-cEOVxwckP5TqzLEYL34EMlY
|
|
124
125
|
unfold/templates/unfold/helpers/app_list_default.html,sha256=vZkw1F7oHOKReNkdHRYjhuNdA1nNdvSD4wbDmf0bnsM,4102
|
125
126
|
unfold/templates/unfold/helpers/boolean.html,sha256=p_WOlytoXvDwta76WgcV4JSWKpBgKf4amhqmHF798F8,564
|
126
127
|
unfold/templates/unfold/helpers/breadcrumb_item.html,sha256=k_1j57UV0WtzFFlMKaewj4NLbR_DhXI6RzCHThblZLw,234
|
127
|
-
unfold/templates/unfold/helpers/display_header.html,sha256=
|
128
|
+
unfold/templates/unfold/helpers/display_header.html,sha256=J49ReIVl8Z29714HMIH-zFfaQdraMRS32VL1Ewg4W9M,808
|
128
129
|
unfold/templates/unfold/helpers/display_label.html,sha256=LS9DWzYjHkYLV27sZDwyXlg2sLJ0AlId9FbjnXpsbfg,317
|
129
130
|
unfold/templates/unfold/helpers/field.html,sha256=oEhGUrLZi2hiuLaC96R2zdwD8DNZqX2_sJIxTpPTJDM,340
|
130
131
|
unfold/templates/unfold/helpers/field_readonly.html,sha256=v7-2oSSDgOsuYpP70y8DqdBqbRybubAfSDzstveoBuw,382
|
@@ -172,8 +173,8 @@ unfold/templatetags/unfold_list.py,sha256=5xAjQX0_JnVwDaj-wGkGqbjOAtp-a18koWIKj5
|
|
172
173
|
unfold/typing.py,sha256=1P8PWM2oeaceUJtA5j071RbKEBpHYaux441u7Hd6wv4,643
|
173
174
|
unfold/utils.py,sha256=5OIgDcwvIJQbwbnnqHx61cHh-2T1h184mTAuNq5WXLI,4088
|
174
175
|
unfold/views.py,sha256=Ml3XlEoHLcbEWof59Dw8ihKBMcmp-gBAibThtBFj55A,708
|
175
|
-
unfold/widgets.py,sha256=
|
176
|
-
django_unfold-0.
|
177
|
-
django_unfold-0.
|
178
|
-
django_unfold-0.
|
179
|
-
django_unfold-0.
|
176
|
+
unfold/widgets.py,sha256=JfcWOWtPPxqosXMyP-BcfO6UK53eoEU3obRpMZijED4,14216
|
177
|
+
django_unfold-0.24.0.dist-info/LICENSE.md,sha256=Ltk_quRyyvV3J5v3brtOqmibeZSw2Hrb8bY1W3ya0Ik,1077
|
178
|
+
django_unfold-0.24.0.dist-info/METADATA,sha256=ph7g7IiHyDYbuoztj_K9eodilCfHAXFTjU_29nuGwHg,46669
|
179
|
+
django_unfold-0.24.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
180
|
+
django_unfold-0.24.0.dist-info/RECORD,,
|
unfold/admin.py
CHANGED
@@ -62,7 +62,7 @@ from .widgets import (
|
|
62
62
|
UnfoldAdminMoneyWidget,
|
63
63
|
UnfoldAdminNullBooleanSelectWidget,
|
64
64
|
UnfoldAdminRadioSelectWidget,
|
65
|
-
|
65
|
+
UnfoldAdminSelectWidget,
|
66
66
|
UnfoldAdminSingleDateWidget,
|
67
67
|
UnfoldAdminSingleTimeWidget,
|
68
68
|
UnfoldAdminSplitDateTimeWidget,
|
@@ -294,7 +294,7 @@ class ModelAdminMixin:
|
|
294
294
|
radio_style=self.radio_fields[db_field.name]
|
295
295
|
)
|
296
296
|
else:
|
297
|
-
kwargs["widget"] =
|
297
|
+
kwargs["widget"] = UnfoldAdminSelectWidget()
|
298
298
|
|
299
299
|
kwargs["choices"] = db_field.get_choices(
|
300
300
|
include_blank=db_field.blank, blank_choice=[("", _("Select value"))]
|
@@ -313,7 +313,7 @@ class ModelAdminMixin:
|
|
313
313
|
db_field.name not in self.get_autocomplete_fields(request)
|
314
314
|
and db_field.name not in self.radio_fields
|
315
315
|
):
|
316
|
-
kwargs["widget"] =
|
316
|
+
kwargs["widget"] = UnfoldAdminSelectWidget()
|
317
317
|
kwargs["empty_label"] = _("Select value")
|
318
318
|
|
319
319
|
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
unfold/contrib/filters/admin.py
CHANGED
@@ -17,16 +17,132 @@ from django.db.models.fields import (
|
|
17
17
|
from django.forms import ValidationError
|
18
18
|
from django.http import HttpRequest
|
19
19
|
from django.utils.dateparse import parse_datetime
|
20
|
+
from django.utils.translation import gettext_lazy as _
|
20
21
|
|
21
22
|
from .forms import (
|
23
|
+
DropdownForm,
|
22
24
|
RangeDateForm,
|
23
25
|
RangeDateTimeForm,
|
24
26
|
RangeNumericForm,
|
27
|
+
SearchForm,
|
25
28
|
SingleNumericForm,
|
26
29
|
SliderNumericForm,
|
27
30
|
)
|
28
31
|
|
29
32
|
|
33
|
+
class ValueMixin:
|
34
|
+
def value(self) -> Optional[str]:
|
35
|
+
return (
|
36
|
+
self.lookup_val[0]
|
37
|
+
if self.lookup_val not in EMPTY_VALUES
|
38
|
+
and isinstance(self.lookup_val, List)
|
39
|
+
and len(self.lookup_val) > 0
|
40
|
+
else self.lookup_val
|
41
|
+
)
|
42
|
+
|
43
|
+
|
44
|
+
class DropdownMixin:
|
45
|
+
template = "unfold/filters/filters_field.html"
|
46
|
+
form_class = DropdownForm
|
47
|
+
all_option = ["", _("All")]
|
48
|
+
|
49
|
+
def queryset(self, request, queryset) -> QuerySet:
|
50
|
+
if self.value() not in EMPTY_VALUES:
|
51
|
+
return super().queryset(request, queryset)
|
52
|
+
|
53
|
+
return queryset
|
54
|
+
|
55
|
+
|
56
|
+
class TextFilter(admin.SimpleListFilter):
|
57
|
+
template = "unfold/filters/filters_field.html"
|
58
|
+
form_class = SearchForm
|
59
|
+
|
60
|
+
def has_output(self) -> bool:
|
61
|
+
return True
|
62
|
+
|
63
|
+
def lookups(self, request: HttpRequest, model_admin: ModelAdmin) -> Tuple:
|
64
|
+
return ()
|
65
|
+
|
66
|
+
def choices(self, changelist: ChangeList) -> Tuple[Dict[str, Any], ...]:
|
67
|
+
return (
|
68
|
+
{
|
69
|
+
"form": self.form_class(
|
70
|
+
name=self.parameter_name,
|
71
|
+
label=_("By {}").format(self.title),
|
72
|
+
data={self.parameter_name: self.value()},
|
73
|
+
),
|
74
|
+
},
|
75
|
+
)
|
76
|
+
|
77
|
+
|
78
|
+
class FieldTextFilter(ValueMixin, admin.FieldListFilter):
|
79
|
+
template = "unfold/filters/filters_field.html"
|
80
|
+
form_class = SearchForm
|
81
|
+
|
82
|
+
def __init__(self, field, request, params, model, model_admin, field_path):
|
83
|
+
self.lookup_kwarg = f"{field_path}__icontains"
|
84
|
+
self.lookup_val = params.get(self.lookup_kwarg)
|
85
|
+
super().__init__(field, request, params, model, model_admin, field_path)
|
86
|
+
|
87
|
+
def expected_parameters(self) -> List[str]:
|
88
|
+
return [self.lookup_kwarg]
|
89
|
+
|
90
|
+
def choices(self, changelist: ChangeList) -> Tuple[Dict[str, Any], ...]:
|
91
|
+
return (
|
92
|
+
{
|
93
|
+
"form": self.form_class(
|
94
|
+
label=_("By {}").format(self.title),
|
95
|
+
name=self.lookup_kwarg,
|
96
|
+
data={self.lookup_kwarg: self.value()},
|
97
|
+
),
|
98
|
+
},
|
99
|
+
)
|
100
|
+
|
101
|
+
|
102
|
+
class DropdownFilter(admin.SimpleListFilter):
|
103
|
+
template = "unfold/filters/filters_field.html"
|
104
|
+
form_class = DropdownForm
|
105
|
+
all_option = ["", _("All")]
|
106
|
+
|
107
|
+
def choices(self, changelist: ChangeList) -> Tuple[Dict[str, Any], ...]:
|
108
|
+
return (
|
109
|
+
{
|
110
|
+
"form": self.form_class(
|
111
|
+
label=_("By {}").format(self.title),
|
112
|
+
name=self.parameter_name,
|
113
|
+
choices=[self.all_option, *self.lookup_choices],
|
114
|
+
data={self.parameter_name: self.value()},
|
115
|
+
),
|
116
|
+
},
|
117
|
+
)
|
118
|
+
|
119
|
+
|
120
|
+
class ChoicesDropdownFilter(ValueMixin, DropdownMixin, admin.ChoicesFieldListFilter):
|
121
|
+
def choices(self, changelist: ChangeList):
|
122
|
+
choices = [self.all_option, *self.field.flatchoices]
|
123
|
+
|
124
|
+
yield {
|
125
|
+
"form": self.form_class(
|
126
|
+
label=_("By {}").format(self.title),
|
127
|
+
name=self.lookup_kwarg,
|
128
|
+
choices=choices,
|
129
|
+
data={self.lookup_kwarg: self.value()},
|
130
|
+
),
|
131
|
+
}
|
132
|
+
|
133
|
+
|
134
|
+
class RelatedDropdownFilter(ValueMixin, DropdownMixin, admin.RelatedFieldListFilter):
|
135
|
+
def choices(self, changelist: ChangeList):
|
136
|
+
yield {
|
137
|
+
"form": self.form_class(
|
138
|
+
label=_("By {}").format(self.title),
|
139
|
+
name=self.lookup_kwarg,
|
140
|
+
choices=[self.all_option, *self.lookup_choices],
|
141
|
+
data={self.lookup_kwarg: self.value()},
|
142
|
+
),
|
143
|
+
}
|
144
|
+
|
145
|
+
|
30
146
|
class SingleNumericFilter(admin.FieldListFilter):
|
31
147
|
request = None
|
32
148
|
parameter_name = None
|
unfold/contrib/filters/forms.py
CHANGED
@@ -1,7 +1,35 @@
|
|
1
1
|
from django import forms
|
2
2
|
from django.utils.translation import gettext_lazy as _
|
3
3
|
|
4
|
-
from ...widgets import
|
4
|
+
from ...widgets import (
|
5
|
+
INPUT_CLASSES,
|
6
|
+
UnfoldAdminSelectWidget,
|
7
|
+
UnfoldAdminSplitDateTimeVerticalWidget,
|
8
|
+
UnfoldAdminTextInputWidget,
|
9
|
+
)
|
10
|
+
|
11
|
+
|
12
|
+
class SearchForm(forms.Form):
|
13
|
+
def __init__(self, name, label, *args, **kwargs):
|
14
|
+
super().__init__(*args, **kwargs)
|
15
|
+
|
16
|
+
self.fields[name] = forms.CharField(
|
17
|
+
label=label,
|
18
|
+
required=False,
|
19
|
+
widget=UnfoldAdminTextInputWidget,
|
20
|
+
)
|
21
|
+
|
22
|
+
|
23
|
+
class DropdownForm(forms.Form):
|
24
|
+
def __init__(self, name, label, choices, *args, **kwargs):
|
25
|
+
super().__init__(*args, **kwargs)
|
26
|
+
|
27
|
+
self.fields[name] = forms.ChoiceField(
|
28
|
+
label=label,
|
29
|
+
required=False,
|
30
|
+
choices=choices,
|
31
|
+
widget=UnfoldAdminSelectWidget,
|
32
|
+
)
|
5
33
|
|
6
34
|
|
7
35
|
class SingleNumericForm(forms.Form):
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
{% with choices.0 as choice %}
|
4
4
|
<div class="flex flex-col mb-6">
|
5
|
-
<h3 class="font-medium mb-
|
5
|
+
<h3 class="font-medium mb-2 text-gray-700 text-sm dark:text-gray-200">{% blocktrans with filter_title=title %}By {{ filter_title }}{% endblocktrans %}</h3>
|
6
6
|
|
7
7
|
<div class="flex flex-col space-y-4">
|
8
8
|
{% for field in choice.form %}
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
{% with choices.0 as choice %}
|
4
4
|
<div class="flex flex-col mb-6">
|
5
|
-
<h3 class="font-medium mb-
|
5
|
+
<h3 class="font-medium mb-2 text-gray-700 text-sm dark:text-gray-200">{% blocktrans with filter_title=title %}By {{ filter_title }}{% endblocktrans %}</h3>
|
6
6
|
|
7
7
|
<div class="flex flex-col space-y-4">
|
8
8
|
{% for field in choice.form %}
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
{% with choices.0 as choice %}
|
4
4
|
<div class="flex flex-col mb-6">
|
5
|
-
<h3 class="font-medium mb-
|
5
|
+
<h3 class="font-medium mb-2 text-gray-700 text-sm dark:text-gray-200">{% blocktrans with filter_title=title %}By {{ filter_title }}{% endblocktrans %}</h3>
|
6
6
|
|
7
7
|
<div class="flex flex-row gap-4">
|
8
8
|
{% for field in choice.form %}
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
{% with choices.0 as choice %}
|
4
4
|
<div class="flex flex-col mb-6">
|
5
|
-
<h3 class="font-medium mb-
|
5
|
+
<h3 class="font-medium mb-2 text-gray-700 text-sm dark:text-gray-200">{% blocktrans with filter_title=title %}By {{ filter_title }}{% endblocktrans %}</h3>
|
6
6
|
|
7
7
|
{% for field in choice.form %}
|
8
8
|
<div class="flex flex-row flex-wrap group relative{% if field.errors %} errors{% endif %}">
|
@@ -3,7 +3,7 @@
|
|
3
3
|
|
4
4
|
{% with choices.0 as choice %}
|
5
5
|
<div class="admin-numeric-filter-wrapper mb-6">
|
6
|
-
<h3 class="font-medium mb-
|
6
|
+
<h3 class="font-medium mb-2 text-gray-700 text-sm dark:text-gray-200">
|
7
7
|
{% blocktrans with filter_title=title %}By {{ filter_title }}{% endblocktrans %}
|
8
8
|
</h3>
|
9
9
|
|
@@ -1,16 +1,15 @@
|
|
1
|
-
from import_export.forms import
|
2
|
-
from import_export.forms import ImportForm as BaseImportForm
|
1
|
+
from import_export.forms import ImportExportFormBase as BaseImportExportFormBase
|
3
2
|
from unfold.widgets import SELECT_CLASSES, UnfoldAdminFileFieldWidget
|
4
3
|
|
5
4
|
|
6
|
-
class ImportForm(
|
5
|
+
class ImportForm(BaseImportExportFormBase):
|
7
6
|
def __init__(self, *args, **kwargs):
|
8
7
|
super().__init__(*args, **kwargs)
|
9
|
-
self.fields["
|
10
|
-
self.fields["
|
8
|
+
self.fields["resource"].widget = UnfoldAdminFileFieldWidget()
|
9
|
+
self.fields["format"].widget.attrs["class"] = " ".join(SELECT_CLASSES)
|
11
10
|
|
12
11
|
|
13
|
-
class ExportForm(
|
12
|
+
class ExportForm(BaseImportExportFormBase):
|
14
13
|
def __init__(self, *args, **kwargs):
|
15
14
|
super().__init__(*args, **kwargs)
|
16
|
-
self.fields["
|
15
|
+
self.fields["format"].widget.attrs["class"] = " ".join(SELECT_CLASSES)
|
@@ -37,7 +37,7 @@
|
|
37
37
|
{% csrf_token %}
|
38
38
|
|
39
39
|
<fieldset class="border border-gray-200 mb-8 rounded-md pt-3 px-3 shadow-sm dark:border-gray-800">
|
40
|
-
{% include "unfold/helpers/field.html" with field=form.
|
40
|
+
{% include "unfold/helpers/field.html" with field=form.format %}
|
41
41
|
</fieldset>
|
42
42
|
|
43
43
|
<button type="submit" class="bg-primary-600 border border-transparent font-medium px-3 py-2 rounded-md text-sm text-white">
|
@@ -22,9 +22,9 @@
|
|
22
22
|
</p>
|
23
23
|
|
24
24
|
<fieldset class="border border-gray-200 mb-8 rounded-md pt-3 px-3 shadow-sm dark:border-gray-800">
|
25
|
-
{% include "unfold/helpers/field.html" with field=form.
|
25
|
+
{% include "unfold/helpers/field.html" with field=form.resource %}
|
26
26
|
|
27
|
-
{% include "unfold/helpers/field.html" with field=form.
|
27
|
+
{% include "unfold/helpers/field.html" with field=form.format %}
|
28
28
|
</fieldset>
|
29
29
|
|
30
30
|
|
@@ -1,12 +1,12 @@
|
|
1
1
|
{% load i18n unfold %}
|
2
2
|
|
3
3
|
<div class="mb-6">
|
4
|
-
<h3 class="font-medium mb-
|
4
|
+
<h3 class="font-medium mb-2 text-gray-700 text-sm dark:text-gray-200">
|
5
5
|
{% blocktranslate with filter_title=title %} By {{ filter_title }} {% endblocktranslate %}
|
6
6
|
</h3>
|
7
7
|
|
8
8
|
{% for choice in choices %}
|
9
|
-
{% if choice.selected %}
|
9
|
+
{% if choice.selected and spec.lookup_val.0 %}
|
10
10
|
<input type="hidden" name="{{ spec.lookup_kwarg }}" value="{{ spec.lookup_val.0 }}" />
|
11
11
|
{% endif %}
|
12
12
|
{% endfor %}
|
@@ -1,17 +1,20 @@
|
|
1
1
|
<div class="flex gap-4 items-center">
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
2
|
+
{% if value.3 and value.3.path %}
|
3
|
+
<div class="bg-center bg-cover bg-white border flex font-medium h-8 w-8 dark:bg-gray-900 dark:border-gray-700 {% if value.3.squared %}rounded-sm{% else %}rounded-full{% endif %}" style="background-image: url('{{ value.3.path }}')">
|
4
|
+
</div>
|
5
|
+
{% elif value.2 %}
|
6
|
+
<div class="bg-white border flex font-medium h-8 justify-center items-center rounded-full text-xs uppercase w-8 dark:bg-gray-900 dark:border-gray-700">
|
7
|
+
{{ value.2 }}
|
8
|
+
</div>
|
9
|
+
{% endif %}
|
7
10
|
|
8
|
-
|
9
|
-
|
11
|
+
<div class="flex flex-col text-right lg:text-left">
|
12
|
+
<h3>{{ value.0 }}</h3>
|
10
13
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
{% if value.1 %}
|
15
|
+
<p class="text-gray-500">
|
16
|
+
{{ value.1 }}
|
17
|
+
</p>
|
18
|
+
{% endif %}
|
19
|
+
</div>
|
17
20
|
</div>
|
unfold/widgets.py
CHANGED
@@ -444,7 +444,7 @@ class UnfoldAdminNullBooleanSelectWidget(NullBooleanSelect):
|
|
444
444
|
super().__init__(attrs)
|
445
445
|
|
446
446
|
|
447
|
-
class
|
447
|
+
class UnfoldAdminSelectWidget(Select):
|
448
448
|
def __init__(self, attrs=None, choices=()):
|
449
449
|
if attrs is None:
|
450
450
|
attrs = {}
|
@@ -482,7 +482,7 @@ try:
|
|
482
482
|
def __init__(self, *args, **kwargs):
|
483
483
|
super().__init__(
|
484
484
|
amount_widget=UnfoldAdminTextInputWidget,
|
485
|
-
currency_widget=
|
485
|
+
currency_widget=UnfoldAdminSelectWidget(choices=CURRENCY_CHOICES),
|
486
486
|
)
|
487
487
|
|
488
488
|
except ImportError:
|
File without changes
|
File without changes
|