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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-unfold
3
- Version: 0.23.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
- - **Custom filters:** widgets for filtering number & datetime values
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 "First main heading", "Smaller additional description", "AB"
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=JjfiJhWSdhDBMK3cf6IsCyGwv5povQgAZtzeHWKrEtA,23792
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=7FJl86b407ylTjgkRN2EwXGHnnVNwBiGvA1_822L5uc,16412
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=-Tv1vWh-u8eLPAJqSX6c8R3IY0GmgOoFrzVQ8E8gO_s,4055
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=6OcC2JZAlegj5rYoUqpRT5mLanFxRy7bB2RBAe-NGiI,661
16
- unfold/contrib/filters/templates/unfold/filters/filters_datetime_range.html,sha256=6OcC2JZAlegj5rYoUqpRT5mLanFxRy7bB2RBAe-NGiI,661
17
- unfold/contrib/filters/templates/unfold/filters/filters_numeric_range.html,sha256=pwoSmezSZhvhG2fiAHMYaYdT586IJ13EcnOoHMkc9n4,639
18
- unfold/contrib/filters/templates/unfold/filters/filters_numeric_single.html,sha256=3njmCOx1HLNBz6VWDQtBWXwBUcclY2Y3D1vRVTwNn8c,576
19
- unfold/contrib/filters/templates/unfold/filters/filters_numeric_slider.html,sha256=LqN3D483HbeT33SI4Uoy8YxKLP0uWbKyt8_bi1EWz3w,1681
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=73Jz93HtENiZe7jzuKSPl4JvasXCpDSGZw_9LvJ2QCU,1156
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=dLqLv7YP0i8_CrxupEfh3VncJ8CJHf7O1eQoWFOcydU,672
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=GHzvpihEakCcdKtF1fxkzFkrYi3KZYBWHB1P6PrVAbc,1791
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=wyV-j0EDIxLzFaJaS-OU1YuTYn6536bg5ohILsDcXoc,1312
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=JAp95Mg6W2Pfdrr-gxEZld9EvgMGLj8etMdSl84l4ro,1686
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=E-yG9ydyb6rRIR5TT4FxekD3qokilfoOwaEaB7np8WI,433
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=e2yGwpyO7VT1NoSj0VzzRlmQZIzREAKOgp7CkyZ4hv0,14204
176
- django_unfold-0.23.0.dist-info/LICENSE.md,sha256=Ltk_quRyyvV3J5v3brtOqmibeZSw2Hrb8bY1W3ya0Ik,1077
177
- django_unfold-0.23.0.dist-info/METADATA,sha256=j8ueC2Ym74SeXEcdOxhx1KT4JfRLK7kO1pjj1rmlqdI,42827
178
- django_unfold-0.23.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
179
- django_unfold-0.23.0.dist-info/RECORD,,
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
- UnfoldAdminSelect,
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"] = UnfoldAdminSelect()
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"] = UnfoldAdminSelect()
316
+ kwargs["widget"] = UnfoldAdminSelectWidget()
317
317
  kwargs["empty_label"] = _("Select value")
318
318
 
319
319
  return super().formfield_for_foreignkey(db_field, request, **kwargs)
@@ -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
@@ -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 INPUT_CLASSES, UnfoldAdminSplitDateTimeVerticalWidget
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-4 text-gray-700 text-sm dark:text-gray-200">{% blocktrans with filter_title=title %}By {{ filter_title }}{% endblocktrans %}</h3>
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-4 text-gray-700 text-sm dark:text-gray-200">{% blocktrans with filter_title=title %}By {{ filter_title }}{% endblocktrans %}</h3>
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 %}
@@ -0,0 +1,7 @@
1
+ {% load i18n %}
2
+
3
+ {% with choices.0 as choice %}
4
+ {% for field in choice.form %}
5
+ {% include "unfold/helpers/field.html" %}
6
+ {% endfor %}
7
+ {% endwith %}
@@ -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-4 text-gray-700 text-sm dark:text-gray-200">{% blocktrans with filter_title=title %}By {{ filter_title }}{% endblocktrans %}</h3>
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-4 text-gray-700 text-sm dark:text-gray-200">{% blocktrans with filter_title=title %}By {{ filter_title }}{% endblocktrans %}</h3>
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-4 text-gray-700 text-sm dark:text-gray-200">
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
 
@@ -7,7 +7,7 @@ from unfold.widgets import SELECT_CLASSES
7
7
 
8
8
  def export_action_form_factory(formats):
9
9
  class _ExportActionForm(ActionForm):
10
- file_format = forms.ChoiceField(
10
+ format = forms.ChoiceField(
11
11
  label=" ",
12
12
  choices=formats,
13
13
  required=False,
@@ -1,16 +1,15 @@
1
- from import_export.forms import ExportForm as BaseExportForm
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(BaseImportForm):
5
+ class ImportForm(BaseImportExportFormBase):
7
6
  def __init__(self, *args, **kwargs):
8
7
  super().__init__(*args, **kwargs)
9
- self.fields["import_file"].widget = UnfoldAdminFileFieldWidget()
10
- self.fields["input_format"].widget.attrs["class"] = " ".join(SELECT_CLASSES)
8
+ self.fields["resource"].widget = UnfoldAdminFileFieldWidget()
9
+ self.fields["format"].widget.attrs["class"] = " ".join(SELECT_CLASSES)
11
10
 
12
11
 
13
- class ExportForm(BaseExportForm):
12
+ class ExportForm(BaseImportExportFormBase):
14
13
  def __init__(self, *args, **kwargs):
15
14
  super().__init__(*args, **kwargs)
16
- self.fields["file_format"].widget.attrs["class"] = " ".join(SELECT_CLASSES)
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.file_format %}
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.import_file %}
25
+ {% include "unfold/helpers/field.html" with field=form.resource %}
26
26
 
27
- {% include "unfold/helpers/field.html" with field=form.input_format %}
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 text-gray-700 text-sm dark:text-gray-200">
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
- {% if value.2 %}
3
- <div class="border flex font-medium h-8 justify-center items-center rounded-full text-xs uppercase w-8 dark:border-gray-700">
4
- {{ value.2 }}
5
- </div>
6
- {% endif %}
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
- <div class="flex flex-col text-right lg:text-left">
9
- <h3>{{ value.0 }}</h3>
11
+ <div class="flex flex-col text-right lg:text-left">
12
+ <h3>{{ value.0 }}</h3>
10
13
 
11
- {% if value.1 %}
12
- <p class="text-gray-500">
13
- {{ value.1 }}
14
- </p>
15
- {% endif %}
16
- </div>
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 UnfoldAdminSelect(Select):
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=UnfoldAdminSelect(choices=CURRENCY_CHOICES),
485
+ currency_widget=UnfoldAdminSelectWidget(choices=CURRENCY_CHOICES),
486
486
  )
487
487
 
488
488
  except ImportError: