Skip to content

Add support for conditional_fields to TabularInline and StackedInline #1482

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions docs/configuration/conditional-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 2 additions & 0 deletions src/unfold/mixins/base_model_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions src/unfold/templates/admin/edit_inline/stacked.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ <h2 id="{{ inline_admin_formset.formset.prefix }}-heading" class="inline-heading
{% endif %}>

{% for inline_admin_form in inline_admin_formset %}
<div x-sort:item class="inline-related group inline-stacked {% if inline_admin_form.original or inline_admin_form.show_url %} has_original{% endif %}{% if forloop.last and inline_admin_formset.has_add_permission %} empty-form last-related{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}" {% if inline_admin_formset.opts.collapsible and inline_admin_form.original %}x-data="{ open: {% if inline_admin_form.errors %}true{% else %}false{% endif %} }"{% endif %}>
<div x-sort:item class="inline-related group inline-stacked {% if inline_admin_form.original or inline_admin_form.show_url %} has_original{% endif %}{% if forloop.last and inline_admin_formset.has_add_permission %} empty-form last-related{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}" {% if inline_admin_formset.opts.collapsible and inline_admin_form.original %}x-data="{ open: {% if inline_admin_form.errors %}true{% else %}false{% endif %} }"{% endif %}{% if inline_admin_form.model_admin.conditional_fields %} x-data='{{ inline_admin_form|changeform_data }}'{% endif %}>
{% if not inline_admin_formset.opts.hide_title or inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission %}
<h3 class="border-b border-base-200 flex font-medium items-center gap-2 px-3 py-2 text-sm dark:border-base-800 {% if inline_admin_formset.opts.collapsible and inline_admin_form.original %}cursor-pointer{% endif %}" {% if inline_admin_formset.opts.collapsible and inline_admin_form.original %}x-on:click="open = !open"{% endif %}>
{% if inline_admin_formset.opts.collapsible and inline_admin_form.original %}
Expand Down Expand Up @@ -91,7 +91,7 @@ <h3 class="border-b border-base-200 flex font-medium items-center gap-2 px-3 py-
<div class="border-b border-base-200 dark:border-base-800" {% if inline_admin_formset.opts.collapsible and inline_admin_form.original %}x-show="open"{% endif %}>
{% for fieldset in inline_admin_form %}
<div class="{% if inline_admin_formset.opts.hide_title %}{% if not inline_admin_formset.formset.can_delete or not inline_admin_formset.has_delete_permission %}pt-3{% endif %}{% endif %}">
{% include 'admin/includes/fieldset.html' with stacked=1 %}
{% include 'admin/includes/fieldset.html' with stacked=1 adminform=inline_admin_form has_conditional_display=inline_admin_form.model_admin.conditional_fields %}
</div>
{% endfor %}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/unfold/templates/admin/edit_inline/tabular.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ <h2 id="{{ inline_admin_formset.formset.prefix }}-heading" class="bg-base-100 bo
{% include "unfold/helpers/edit_inline/tabular_heading.html" %}

{% for inline_admin_form in inline_admin_formset %}
<tbody class="{% if inline_admin_form.original or inline_admin_form.show_url %}has_original {% else %}template{% endif %}" x-sort:item>
<tbody class="{% if inline_admin_form.original or inline_admin_form.show_url %}has_original {% else %}template{% endif %}" x-sort:item{% if inline_admin_form.model_admin.conditional_fields %} x-data='{{ inline_admin_form|changeform_data }}'{% endif %}>
{% include "unfold/helpers/edit_inline/tabular_error.html" %}

{% include "unfold/helpers/edit_inline/tabular_title.html" %}
Expand Down
15 changes: 10 additions & 5 deletions src/unfold/templates/unfold/helpers/edit_inline/tabular_row.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{% load unfold %}
<tr class="form-row {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}{% if forloop.last and inline_admin_formset.has_add_permission %} empty-form{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
{% spaceless %}
{% for fieldset in inline_admin_form %}
Expand All @@ -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 %}
<td{% if field.field.name %} class="min-h-[62.5px] border-b max-md:border-b border-base-200 dark:border-base-800 field-{{ field.field.name }} group field-tabular {% if field.field.errors|length > 0 %} errors{% endif %}{% if inline_admin_form.original %} p-3 lg:py-3{% else %} py-3{% endif %}{% if field.is_checkbox %} align-middle{% else %} align-top{% endif %} flex items-center before:capitalize before:font-semibold before:content-[attr(data-label)] before:text-font-important-light dark:before:text-font-important-dark before:mr-auto before:font-text before:pr-4 lg:before:hidden font-normal px-3 lg:first:pl-3 lg:last:pr-3 lg:px-1.5 text-left lg:table-cell {% if field.field.is_hidden %} hidden!{% endif %} {% if inline_admin_formset.opts.ordering_field and field.field.name == inline_admin_formset.opts.ordering_field and inline_admin_formset.opts.hide_ordering_field %}hidden!{% endif %}"{% endif %} data-label="{{ field.field.label }}">
{% include "unfold/helpers/edit_inline/tabular_field.html" %}
</td>
{% 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 %}
<td{% if field.field.name %} class="min-h-[62.5px] border-b max-md:border-b border-base-200 dark:border-base-800 field-{{ field.field.name }} group field-tabular {% if field.field.errors|length > 0 %} errors{% endif %}{% if inline_admin_form.original %} p-3 lg:py-3{% else %} py-3{% endif %}{% if field.is_checkbox %} align-middle{% else %} align-top{% endif %} flex items-center before:capitalize before:font-semibold before:content-[attr(data-label)] before:text-font-important-light dark:before:text-font-important-dark before:mr-auto before:font-text before:pr-4 lg:before:hidden font-normal px-3 lg:first:pl-3 lg:last:pr-3 lg:px-1.5 text-left lg:table-cell {% if field.field.is_hidden %} hidden!{% endif %} {% if inline_admin_formset.opts.ordering_field and field.field.name == inline_admin_formset.opts.ordering_field and inline_admin_formset.opts.hide_ordering_field %}hidden!{% endif %}"{% endif %} data-label="{{ field.field.label }}" {% if conditional_display %} x-bind:class="{collapse: {{ conditional_display }}}"{% endif %}>
{% include "unfold/helpers/edit_inline/tabular_field.html" %}
</td>
{% endwith %}
{% endif %}
{% endwith %}
{% endfor %}
{% endfor %}
{% endfor %}
Expand Down
61 changes: 59 additions & 2 deletions tests/server/example/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@
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.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
from .models import ActionUser, NotableUser, SectionUser, Tag, User, UserNote

admin.site.unregister(Group)

Expand All @@ -21,6 +25,59 @@ class UserTagInline(StackedInline):
collapsible = True


class UserNoteTabularInline(TabularInline):
model = UserNote
conditional_fields = {
"note": "type == 'note'",
"tag": "type == 'tag'",
}


class UserNoteStackedInline(StackedInline):
model = UserNote
conditional_fields = {
"note": "type == 'note'",
"tag": "type == 'tag'",
}


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,
UserTagUnrelatedStackedInline,
UserTagUnrelatedTabularInline,
)


@admin.register(User)
class UserAdmin(BaseUserAdmin, ModelAdmin):
form = UserChangeForm
Expand Down
57 changes: 57 additions & 0 deletions tests/server/example/migrations/0005_notableuser_usernote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Generated by Django 4.2.22 on 2025-07-30 11:31

import django.contrib.auth.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"),
]

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,
),
),
],
),
]
12 changes: 12 additions & 0 deletions tests/server/example/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,15 @@ 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