django-admin-radio-select
Synchronize BooleanFields across Django Admin inline rows as mutually exclusive radio button groups.
Problem
An inline BooleanField column renders as independent checkboxes:
Image A [ ]
Image B [x]
Image C [ ]
Nothing stops you from checking more than one — but often only one row should ever be true (the primary image, the default address, the featured item). This package renders that column as radio buttons instead:
Image A ( )
Image B (•)
Image C ( )
Selecting one clears the others, enforced with a small vanilla JS module. Every row still POSTs under its own normal Django-generated field name (e.g. images-0-is_primary, images-1-is_primary) — nothing here relies on giving different forms the same HTML name. A formset-level check on the server also rejects a handcrafted POST that tries to set more than one row true, so the guarantee doesn't depend on JavaScript running at all.
Inline formset, before and after clicking a different row's radio — note is_primary switches from "Snowy Peak" to "Frozen Lake" while the independent is_featured column ("Cabin") is untouched:
Same behavior on a ModelAdmin changelist, via list_editable (see below):
Install
uv add django-admin-radio-select
# or
pip install django-admin-radio-select
Add it to INSTALLED_APPS — it's a real Django app (with its own AppConfig), which is what makes django.contrib.staticfiles pick up its JS:
INSTALLED_APPS = [
...,
"django_admin_radio_select",
]
Usage
from django.contrib import admin
from django_admin_radio_select import ExclusiveRadioFieldsMixin
from .models import Album, Image
class ImageInline(ExclusiveRadioFieldsMixin, admin.TabularInline):
model = Image
radio_select_exclusive_fields = ("is_primary",)
@admin.register(Album)
class AlbumAdmin(admin.ModelAdmin):
inlines = [ImageInline]
Works the same way with StackedInline:
class ImageInline(ExclusiveRadioFieldsMixin, admin.StackedInline):
model = Image
radio_select_exclusive_fields = ("is_primary",)
Multiple fields are independent groups:
class ImageInline(ExclusiveRadioFieldsMixin, admin.TabularInline):
model = Image
radio_select_exclusive_fields = ("is_primary", "is_featured")
Compute the field list per request (and, for an inline or a change form, per object) instead of (or in addition to) the class attribute:
class ImageInline(ExclusiveRadioFieldsMixin, admin.TabularInline):
model = Image
def get_radio_select_exclusive_fields(self, request, obj=None):
return ("is_primary",) if request.user.is_superuser else ()
Whatever get_radio_select_exclusive_fields returns — the default above, or an override like the one above it — always gets filtered against get_readonly_fields(request, obj) before being used: a readonly field isn't rendered as an editable widget at all, so there's nothing to turn into a radio button. This matters for a field that's only conditionally readonly (permissions, object state, ...): without it, a field in radio_select_exclusive_fields that becomes readonly for some request would no longer be on the form at all and raise ImproperlyConfigured, for a state the admin class itself put it in. This filtering happens in a separate _get_effective_radio_select_exclusive_fields(request, obj) method precisely so overriding get_radio_select_exclusive_fields doesn't also have to reimplement it.
Each configured name must be a BooleanField present on the form — anything else raises ImproperlyConfigured when the admin builds the formset (a python manage.py check-time-ish failure, not a silent no-op). "Present on the form" is deliberate: the field only has to exist there, not on the model. A field declared only on a custom ModelForm/InlineModelAdmin.form (not a real model field at all) works exactly the same as any other configured field, and so does a form field that overrides the model's own type for that name (e.g. the model has a CharField but the form redeclares it as a BooleanField) — this package never inspects the model, only form.base_fields.
ModelAdmin changelist (list_editable)
The same mixin also works directly on a ModelAdmin, for a BooleanField column made editable in the changelist via list_editable. The field still has to be in list_display too — that's a normal Django list_editable requirement, unrelated to this package:
@admin.register(Album)
class AlbumAdmin(ExclusiveRadioFieldsMixin, admin.ModelAdmin):
list_display = ("title", "is_featured")
list_editable = ("is_featured",)
radio_select_exclusive_fields = ("is_featured",)
Now only one Album row in the whole changelist can have is_featured checked at a time — same client-side sync, same per-row Django field names (form-0-is_featured, form-1-is_featured, ...), same server-side exclusivity check on save.
Themes
- django-unfold: works as it is, with Unfold's radios.
Supported versions
Python 3.10–3.13, Django 4.2–5.2. Works on TabularInline, StackedInline, and ModelAdmin (via list_editable).
Development
See CONTRIBUTING.md for setup, running the example project, and the test/lint/type-check commands.
Metadata
Release files for django-admin-radio-select 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_admin_radio_select-0.1.1.tar.gz | 377.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_admin_radio_select-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 388.4 kB
Release files / django_admin_radio_select-0.1.1.tar.gz
| Download URL | django_admin_radio_select-0.1.1.tar.gz |
|---|---|
| Size | 377.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
eab21d726f547a2c5566823eaa236ab7a2df1920468aba6edc5e71507dc8d591
|
|
BLAKE2b-256 checksum How to use checksums |
b20f9aa0eee48ced74a9f8b85ca7fa8d97c3435e02d5974dddc6adb5445f5f12
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency logRelease files / django_admin_radio_select-0.1.1-py3-none-any.whl
| Download URL | django_admin_radio_select-0.1.1-py3-none-any.whl |
|---|---|
| Size | 11.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
363d8792d62be42df66735c42cd1b02003163336c3fa9433a27fbc56609188d3
|
|
BLAKE2b-256 checksum How to use checksums |
09f8bc895796d724d64a073d48f55b5c148126d4f854bef07f5f77786055e23b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency log