From 15a0fdb383f38185cf8b961dadf5ae64ed971a0c Mon Sep 17 00:00:00 2001 From: Take Weiland Date: Wed, 30 Jul 2025 19:39:23 +0200 Subject: [PATCH 1/9] Basic implementation of conditional fields for tabular inline --- .../templates/admin/edit_inline/tabular.html | 2 +- .../helpers/edit_inline/tabular_field.html | 2 +- .../helpers/edit_inline/tabular_row.html | 15 ++++--- tests/server/example/admin.py | 18 ++++++++- .../migrations/0005_notableuser_usernote.py | 40 +++++++++++++++++++ tests/server/example/models.py | 16 ++++++++ 6 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 tests/server/example/migrations/0005_notableuser_usernote.py diff --git a/src/unfold/templates/admin/edit_inline/tabular.html b/src/unfold/templates/admin/edit_inline/tabular.html index eb9f25f4a..acc0a25a9 100644 --- a/src/unfold/templates/admin/edit_inline/tabular.html +++ b/src/unfold/templates/admin/edit_inline/tabular.html @@ -23,7 +23,7 @@

+ {% include "unfold/helpers/edit_inline/tabular_error.html" %} {% include "unfold/helpers/edit_inline/tabular_title.html" %} diff --git a/src/unfold/templates/unfold/helpers/edit_inline/tabular_field.html b/src/unfold/templates/unfold/helpers/edit_inline/tabular_field.html index 013010588..40da19853 100644 --- a/src/unfold/templates/unfold/helpers/edit_inline/tabular_field.html +++ b/src/unfold/templates/unfold/helpers/edit_inline/tabular_field.html @@ -1,4 +1,4 @@ -
+
{% if forloop.parentloop.counter == 1 and forloop.counter == 1 %} {% if inline_admin_formset.opts.ordering_field %} {% if inline_admin_form.original %} diff --git a/src/unfold/templates/unfold/helpers/edit_inline/tabular_row.html b/src/unfold/templates/unfold/helpers/edit_inline/tabular_row.html index 1d9547c8e..20f21d31f 100644 --- a/src/unfold/templates/unfold/helpers/edit_inline/tabular_row.html +++ b/src/unfold/templates/unfold/helpers/edit_inline/tabular_row.html @@ -1,3 +1,4 @@ +{% load unfold %} {% spaceless %} {% for fieldset in inline_admin_form %} @@ -14,11 +15,15 @@ {% for fieldset in inline_admin_form %} {% for line in fieldset %} {% for field in line %} - {% if field.is_readonly or not field.field.is_hidden %} - - {% include "unfold/helpers/edit_inline/tabular_field.html" %} - - {% endif %} + {% with inline_admin_form.model_admin.conditional_fields|index:field.field.name as conditional_display %} + {% if field.is_readonly or not field.field.is_hidden %} + {% with field|changeform_condition as field %} + + {% include "unfold/helpers/edit_inline/tabular_field.html" %} + + {% endwith %} + {% endif %} + {% endwith %} {% endfor %} {% endfor %} {% endfor %} diff --git a/tests/server/example/admin.py b/tests/server/example/admin.py index f3958d689..71007c55f 100644 --- a/tests/server/example/admin.py +++ b/tests/server/example/admin.py @@ -5,12 +5,12 @@ from django.shortcuts import redirect from django.urls import reverse_lazy -from unfold.admin import ModelAdmin, StackedInline +from unfold.admin import ModelAdmin, StackedInline, TabularInline from unfold.decorators import action from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm from unfold.sections import TableSection, TemplateSection -from .models import ActionUser, SectionUser, Tag, User +from .models import ActionUser, SectionUser, Tag, User, NotableUser, UserNote admin.site.unregister(Group) @@ -21,6 +21,20 @@ class UserTagInline(StackedInline): collapsible = True +class UserNoteInline(TabularInline): + model = UserNote + conditional_fields = { + "note": "type == 'note'", + "tag": "type == 'tag'" + } + + +@admin.register(NotableUser) +class NotableUserAdmin(ModelAdmin): + fields = ('username', ) + inlines = (UserNoteInline, ) + + @admin.register(User) class UserAdmin(BaseUserAdmin, ModelAdmin): form = UserChangeForm diff --git a/tests/server/example/migrations/0005_notableuser_usernote.py b/tests/server/example/migrations/0005_notableuser_usernote.py new file mode 100644 index 000000000..faab2cc47 --- /dev/null +++ b/tests/server/example/migrations/0005_notableuser_usernote.py @@ -0,0 +1,40 @@ +# Generated by Django 4.2.22 on 2025-07-30 11:31 + +from django.conf import settings +import django.contrib.auth.models +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('example', '0004_actionuser_sectionuser'), + ] + + operations = [ + migrations.CreateModel( + name='NotableUser', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('example.user',), + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='UserNote', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.CharField(choices=[('note', 'Note'), ('tag', 'Tag')], max_length=16)), + ('note', models.CharField(blank=True, max_length=255)), + ('tag', models.CharField(blank=True, max_length=255)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/tests/server/example/models.py b/tests/server/example/models.py index ac43dc16c..76ee251f3 100644 --- a/tests/server/example/models.py +++ b/tests/server/example/models.py @@ -24,3 +24,19 @@ class Tag(models.Model): def __str__(self): return self.name + + +class UserNote(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + type = models.CharField(choices=[ + ('note', 'Note'), + ('tag', 'Tag') + ], max_length=16) + note = models.CharField(max_length=255, blank=True) + tag = models.CharField(max_length=255, blank=True) + + +class NotableUser(User): + + class Meta: + proxy = True From 86b51bef023f1bb19b2c9ecc15fd9b276af8a175 Mon Sep 17 00:00:00 2001 From: Take Weiland Date: Wed, 30 Jul 2025 19:49:00 +0200 Subject: [PATCH 2/9] Use visibility: collapse for hiding tabular inline fields --- .../templates/unfold/helpers/edit_inline/tabular_field.html | 2 +- .../templates/unfold/helpers/edit_inline/tabular_row.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/unfold/templates/unfold/helpers/edit_inline/tabular_field.html b/src/unfold/templates/unfold/helpers/edit_inline/tabular_field.html index 40da19853..013010588 100644 --- a/src/unfold/templates/unfold/helpers/edit_inline/tabular_field.html +++ b/src/unfold/templates/unfold/helpers/edit_inline/tabular_field.html @@ -1,4 +1,4 @@ -
+
{% if forloop.parentloop.counter == 1 and forloop.counter == 1 %} {% if inline_admin_formset.opts.ordering_field %} {% if inline_admin_form.original %} diff --git a/src/unfold/templates/unfold/helpers/edit_inline/tabular_row.html b/src/unfold/templates/unfold/helpers/edit_inline/tabular_row.html index 20f21d31f..1399374b0 100644 --- a/src/unfold/templates/unfold/helpers/edit_inline/tabular_row.html +++ b/src/unfold/templates/unfold/helpers/edit_inline/tabular_row.html @@ -18,7 +18,7 @@ {% with inline_admin_form.model_admin.conditional_fields|index:field.field.name as conditional_display %} {% if field.is_readonly or not field.field.is_hidden %} {% with field|changeform_condition as field %} - + {% include "unfold/helpers/edit_inline/tabular_field.html" %} {% endwith %} From 9ea6402ec3717033828d600775a7e5c34f204022 Mon Sep 17 00:00:00 2001 From: Take Weiland Date: Wed, 30 Jul 2025 19:59:47 +0200 Subject: [PATCH 3/9] conditional fields for stacked inlines --- src/unfold/templates/admin/edit_inline/stacked.html | 4 ++-- tests/server/example/admin.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/unfold/templates/admin/edit_inline/stacked.html b/src/unfold/templates/admin/edit_inline/stacked.html index c0193f305..ad2c05d97 100644 --- a/src/unfold/templates/admin/edit_inline/stacked.html +++ b/src/unfold/templates/admin/edit_inline/stacked.html @@ -23,7 +23,7 @@

+ diff --git a/tests/server/example/admin.py b/tests/server/example/admin.py index 71007c55f..3171976d8 100644 --- a/tests/server/example/admin.py +++ b/tests/server/example/admin.py @@ -21,7 +21,15 @@ class UserTagInline(StackedInline): collapsible = True -class UserNoteInline(TabularInline): +class UserNoteTabularInline(TabularInline): + model = UserNote + conditional_fields = { + "note": "type == 'note'", + "tag": "type == 'tag'" + } + + +class UserNoteStackedInline(StackedInline): model = UserNote conditional_fields = { "note": "type == 'note'", @@ -32,7 +40,7 @@ class UserNoteInline(TabularInline): @admin.register(NotableUser) class NotableUserAdmin(ModelAdmin): fields = ('username', ) - inlines = (UserNoteInline, ) + inlines = (UserNoteTabularInline, UserNoteStackedInline) @admin.register(User) From 4fcc2e49bc634336acf07b453c078c15176ecccd Mon Sep 17 00:00:00 2001 From: Take Weiland Date: Wed, 30 Jul 2025 20:20:01 +0200 Subject: [PATCH 4/9] Add examples for nonrelated inline with conditional fields --- tests/server/example/admin.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/server/example/admin.py b/tests/server/example/admin.py index 3171976d8..347986433 100644 --- a/tests/server/example/admin.py +++ b/tests/server/example/admin.py @@ -6,6 +6,7 @@ from django.urls import reverse_lazy from unfold.admin import ModelAdmin, StackedInline, TabularInline +from unfold.contrib.inlines.admin import NonrelatedStackedInline, NonrelatedTabularInline from unfold.decorators import action from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm from unfold.sections import TableSection, TemplateSection @@ -37,10 +38,33 @@ class UserNoteStackedInline(StackedInline): } +class UserTagUnrelatedInlineBase: + model = UserNote + conditional_fields = { + "note": "type == 'note'", + "tag": "type == 'tag'" + } + + def get_form_queryset(self, obj: User): + return self.model.objects.all() + + def save_new_instance(self, parent, instance): + pass + + +class UserTagUnrelatedStackedInline(UserTagUnrelatedInlineBase, NonrelatedStackedInline): + pass + + +class UserTagUnrelatedTabularInline(UserTagUnrelatedInlineBase, NonrelatedTabularInline): + pass + + @admin.register(NotableUser) class NotableUserAdmin(ModelAdmin): fields = ('username', ) - inlines = (UserNoteTabularInline, UserNoteStackedInline) + inlines = (UserNoteTabularInline, UserNoteStackedInline, UserTagUnrelatedStackedInline, + UserTagUnrelatedTabularInline) @admin.register(User) From 5686d547ba034e65680e3cd5bc6b22eef905a6ca Mon Sep 17 00:00:00 2001 From: Take Weiland Date: Wed, 30 Jul 2025 20:28:24 +0200 Subject: [PATCH 5/9] Add docs for conditional_fields in inlines --- docs/configuration/conditional-fields.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/configuration/conditional-fields.md b/docs/configuration/conditional-fields.md index 424ee1541..ef7374b82 100644 --- a/docs/configuration/conditional-fields.md +++ b/docs/configuration/conditional-fields.md @@ -66,3 +66,7 @@ class UserAdmin(ModelAdmin): "address": "different_address == true" } ``` + +### Support + +`conditional_fields` can be used in `ModelAdmin`, `TabularInline` and `StackedInline`. When used with `TabularInline`, the table column containing a hidden field will still be shown, but the field itself will be hidden per row. \ No newline at end of file From c038efc36ad95e37b43ad00ad95484bf750d9fb7 Mon Sep 17 00:00:00 2001 From: Take Weiland Date: Wed, 30 Jul 2025 20:28:39 +0200 Subject: [PATCH 6/9] Add conditional_fields declaration to BaseModelAdminMixin for better autocomplete --- src/unfold/mixins/base_model_admin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/unfold/mixins/base_model_admin.py b/src/unfold/mixins/base_model_admin.py index a718fe2f9..a027035ee 100644 --- a/src/unfold/mixins/base_model_admin.py +++ b/src/unfold/mixins/base_model_admin.py @@ -18,6 +18,8 @@ class BaseModelAdminMixin: + conditional_fields: Optional[dict[str, str]] = None + def __init__(self, model: models.Model, admin_site: AdminSite) -> None: overrides = copy.deepcopy(FORMFIELD_OVERRIDES) From b60e08ebf425c48fdd19477a2900b84cd5756737 Mon Sep 17 00:00:00 2001 From: Take Weiland Date: Wed, 30 Jul 2025 20:29:54 +0200 Subject: [PATCH 7/9] Fix wrong heading level in docs --- docs/configuration/conditional-fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/conditional-fields.md b/docs/configuration/conditional-fields.md index ef7374b82..b68f3c5cb 100644 --- a/docs/configuration/conditional-fields.md +++ b/docs/configuration/conditional-fields.md @@ -67,6 +67,6 @@ class UserAdmin(ModelAdmin): } ``` -### Support +## Support `conditional_fields` can be used in `ModelAdmin`, `TabularInline` and `StackedInline`. When used with `TabularInline`, the table column containing a hidden field will still be shown, but the field itself will be hidden per row. \ No newline at end of file From 0789d6b3a46d5abf7aee7d7719662cb01c29fe75 Mon Sep 17 00:00:00 2001 From: Take Weiland Date: Wed, 30 Jul 2025 20:34:27 +0200 Subject: [PATCH 8/9] Run Ruff --- tests/server/example/admin.py | 31 +++++++---- .../migrations/0005_notableuser_usernote.py | 53 ++++++++++++------- tests/server/example/models.py | 6 +-- 3 files changed, 57 insertions(+), 33 deletions(-) diff --git a/tests/server/example/admin.py b/tests/server/example/admin.py index 347986433..e1c6fa2e6 100644 --- a/tests/server/example/admin.py +++ b/tests/server/example/admin.py @@ -6,12 +6,15 @@ from django.urls import reverse_lazy from unfold.admin import ModelAdmin, StackedInline, TabularInline -from unfold.contrib.inlines.admin import NonrelatedStackedInline, NonrelatedTabularInline +from unfold.contrib.inlines.admin import ( + NonrelatedStackedInline, + NonrelatedTabularInline, +) from unfold.decorators import action from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm from unfold.sections import TableSection, TemplateSection -from .models import ActionUser, SectionUser, Tag, User, NotableUser, UserNote +from .models import ActionUser, NotableUser, SectionUser, Tag, User, UserNote admin.site.unregister(Group) @@ -26,7 +29,7 @@ class UserNoteTabularInline(TabularInline): model = UserNote conditional_fields = { "note": "type == 'note'", - "tag": "type == 'tag'" + "tag": "type == 'tag'", } @@ -34,7 +37,7 @@ class UserNoteStackedInline(StackedInline): model = UserNote conditional_fields = { "note": "type == 'note'", - "tag": "type == 'tag'" + "tag": "type == 'tag'", } @@ -42,7 +45,7 @@ class UserTagUnrelatedInlineBase: model = UserNote conditional_fields = { "note": "type == 'note'", - "tag": "type == 'tag'" + "tag": "type == 'tag'", } def get_form_queryset(self, obj: User): @@ -52,19 +55,27 @@ def save_new_instance(self, parent, instance): pass -class UserTagUnrelatedStackedInline(UserTagUnrelatedInlineBase, NonrelatedStackedInline): +class UserTagUnrelatedStackedInline( + UserTagUnrelatedInlineBase, NonrelatedStackedInline +): pass -class UserTagUnrelatedTabularInline(UserTagUnrelatedInlineBase, NonrelatedTabularInline): +class UserTagUnrelatedTabularInline( + UserTagUnrelatedInlineBase, NonrelatedTabularInline +): pass @admin.register(NotableUser) class NotableUserAdmin(ModelAdmin): - fields = ('username', ) - inlines = (UserNoteTabularInline, UserNoteStackedInline, UserTagUnrelatedStackedInline, - UserTagUnrelatedTabularInline) + fields = ("username",) + inlines = ( + UserNoteTabularInline, + UserNoteStackedInline, + UserTagUnrelatedStackedInline, + UserTagUnrelatedTabularInline, + ) @admin.register(User) diff --git a/tests/server/example/migrations/0005_notableuser_usernote.py b/tests/server/example/migrations/0005_notableuser_usernote.py index faab2cc47..fa6b23126 100644 --- a/tests/server/example/migrations/0005_notableuser_usernote.py +++ b/tests/server/example/migrations/0005_notableuser_usernote.py @@ -1,40 +1,57 @@ # Generated by Django 4.2.22 on 2025-07-30 11:31 -from django.conf import settings import django.contrib.auth.models -from django.db import migrations, models import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('example', '0004_actionuser_sectionuser'), + ("example", "0004_actionuser_sectionuser"), ] operations = [ migrations.CreateModel( - name='NotableUser', - fields=[ - ], + name="NotableUser", + fields=[], options={ - 'proxy': True, - 'indexes': [], - 'constraints': [], + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('example.user',), + bases=("example.user",), managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ("objects", django.contrib.auth.models.UserManager()), ], ), migrations.CreateModel( - name='UserNote', + name="UserNote", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('type', models.CharField(choices=[('note', 'Note'), ('tag', 'Tag')], max_length=16)), - ('note', models.CharField(blank=True, max_length=255)), - ('tag', models.CharField(blank=True, max_length=255)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "type", + models.CharField( + choices=[("note", "Note"), ("tag", "Tag")], max_length=16 + ), + ), + ("note", models.CharField(blank=True, max_length=255)), + ("tag", models.CharField(blank=True, max_length=255)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/tests/server/example/models.py b/tests/server/example/models.py index 76ee251f3..a119cc194 100644 --- a/tests/server/example/models.py +++ b/tests/server/example/models.py @@ -28,15 +28,11 @@ def __str__(self): class UserNote(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) - type = models.CharField(choices=[ - ('note', 'Note'), - ('tag', 'Tag') - ], max_length=16) + type = models.CharField(choices=[("note", "Note"), ("tag", "Tag")], max_length=16) note = models.CharField(max_length=255, blank=True) tag = models.CharField(max_length=255, blank=True) class NotableUser(User): - class Meta: proxy = True From 1610b8dfdd15bcfe6657524cdbd0836df782aa3a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 18:37:47 +0000 Subject: [PATCH 9/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/configuration/conditional-fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/conditional-fields.md b/docs/configuration/conditional-fields.md index b68f3c5cb..5026bcafa 100644 --- a/docs/configuration/conditional-fields.md +++ b/docs/configuration/conditional-fields.md @@ -69,4 +69,4 @@ class UserAdmin(ModelAdmin): ## Support -`conditional_fields` can be used in `ModelAdmin`, `TabularInline` and `StackedInline`. When used with `TabularInline`, the table column containing a hidden field will still be shown, but the field itself will be hidden per row. \ No newline at end of file +`conditional_fields` can be used in `ModelAdmin`, `TabularInline` and `StackedInline`. When used with `TabularInline`, the table column containing a hidden field will still be shown, but the field itself will be hidden per row.