Metadata-Version: 2.4
Name: django-dynamic-admin-columns
Version: 0.4.2
Summary: User-controllable, in-database list_display for Django admin: enable, disable and reorder columns at runtime.
Project-URL: Homepage, https://github.com/iplweb/django-dynamic-admin-columns
Project-URL: Repository, https://github.com/iplweb/django-dynamic-admin-columns
Project-URL: Issues, https://github.com/iplweb/django-dynamic-admin-columns/issues
Project-URL: Changelog, https://github.com/iplweb/django-dynamic-admin-columns/blob/main/CHANGELOG.md
Author-email: Michał Pasternak <michal.dtz@gmail.com>
License: MIT License
        
        Copyright (c) 2022-2026 Michał Pasternak <michal.dtz@gmail.com>
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: admin,columns,django,django-admin,dynamic,list_display
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: <3.15,>=3.11
Requires-Dist: django-admin-sortable2>=2.1
Requires-Dist: django>=5.2
Provides-Extra: dev
Requires-Dist: django-webtest>=1.9; extra == 'dev'
Requires-Dist: model-bakery>=1.17; extra == 'dev'
Requires-Dist: pre-commit>=3; extra == 'dev'
Requires-Dist: pytest-django>=4.5; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Description-Content-Type: text/markdown

# django-dynamic-admin-columns

[![PyPI](https://img.shields.io/pypi/v/django-dynamic-admin-columns.svg)](https://pypi.org/project/django-dynamic-admin-columns/)
[![Python versions](https://img.shields.io/pypi/pyversions/django-dynamic-admin-columns.svg)](https://pypi.org/project/django-dynamic-admin-columns/)
[![License](https://img.shields.io/pypi/l/django-dynamic-admin-columns.svg)](https://github.com/iplweb/django-dynamic-admin-columns/blob/main/LICENSE)
[![Tests](https://github.com/iplweb/django-dynamic-admin-columns/actions/workflows/tests.yml/badge.svg)](https://github.com/iplweb/django-dynamic-admin-columns/actions/workflows/tests.yml)

**User-controllable, in-database `list_display` for Django admin.** End-users
enable, disable and reorder columns at runtime through the admin itself — no
code changes, no redeploy.

![Column picker modal open over the Books changelist](docs/picker-modal.png)

## Why

`django.contrib.admin.ModelAdmin.list_display` is a *developer-time* setting.
If end-users want different columns visible, the developer has to ship a code
change. In bibliographic, archival and CRUD-heavy admin sites the column set
varies between users and projects; this library moves that choice into the
database so the admin UI itself is the configuration surface.

Extracted from the [BPP](https://github.com/iplweb/django-bpp) academic
bibliography system where it has run in production since 2022.

## Features

- Drop-in `DynamicColumnsMixin` for any `ModelAdmin`.
- **In-changelist picker.** A *Columns* button in the standard
  `object-tools` area opens a modal where end-users toggle columns and
  reorder them via drag-and-drop — no admin training, no separate
  preferences page.
- **Per-user layouts.** Each staff user keeps their own column
  configuration; users without a personal layout fall back to the
  global defaults. Resetting is one click away.
- Three column tiers: **always** (pinned, code-only), **default** (visible
  out of the box, user can toggle), **allowed** (hidden by default,
  user-discoverable).
- Per-admin and project-wide regex denylists (`list_display_forbidden`,
  `DYNAMIC_ADMIN_COLUMNS_FORBIDDEN_COLUMN_NAMES`) to keep sensitive or
  noisy fields out of the picker.
- `"__all__"` shorthand: expose every model field and let the user pick.
- Dictionary form of `list_select_related` that activates joins only for
  columns that are actually visible — no overhead for columns the user
  has hidden.
- Vanilla-JS picker UI — **no SortableJS, no jQuery, no Bootstrap**.
- Settings-gated import allowlist
  (`DYNAMIC_ADMIN_COLUMNS_ALLOWED_IMPORT_PATHS`) to prevent arbitrary
  class loading from untrusted database content.
- Polish translation included.

## Installation

```bash
uv add django-dynamic-admin-columns
# or
pip install django-dynamic-admin-columns
```

Add the apps and the import allowlist to your settings:

```python
INSTALLED_APPS = [
    # ...
    "adminsortable2",
    "dynamic_admin_columns",
]

DYNAMIC_ADMIN_COLUMNS_ALLOWED_IMPORT_PATHS = [
    "myapp.admin",
]

# Optional global regex denylist applied to every dynamic admin.
DYNAMIC_ADMIN_COLUMNS_FORBIDDEN_COLUMN_NAMES = [
    r".*_cache$",
    r"^cached_.*",
]

# django-admin-sortable2's reordering view triggers admin.E117 with a dict
# ``list_select_related`` — silence it.
SILENCED_SYSTEM_CHECKS = ["admin.E117"]
```

Then run migrations:

```bash
python manage.py migrate dynamic_admin_columns
```

## Usage

```python
# myapp/admin.py
from django.contrib import admin
from dynamic_admin_columns.mixins import DynamicColumnsMixin

from myapp.models import Book


@admin.register(Book)
class BookAdmin(DynamicColumnsMixin, admin.ModelAdmin):
    # Pinned columns: always visible, always first, cannot be toggled.
    list_display_always = ["title"]

    # Visible out of the box, user can hide or reorder.
    list_display_default = ["author", "isbn"]

    # Hidden by default, user can enable via the admin.
    list_display_allowed = ["pages", "notes"]

    # Per-admin denylist (regex). Wins over ``__all__``.
    list_display_forbidden = [r"^legacy_.*"]
```

First time a user opens the changelist, the matching `ModelAdmin` row
and its `ModelAdminColumn` rows are created automatically as the
**global defaults** (`user IS NULL`). Subsequent edits through the
in-changelist *Columns* picker write to that user's **personal copy**
(`user=<request.user>`); users without a personal copy fall back to
the global row.

### Superuser: editing global defaults

A superuser sees an extra radio switch at the top of the modal:

- **My personal layout** — the default. Saves create or update a
  personal row that affects only the current user.
- **Global defaults** — saves rewrite the `user IS NULL` row, so
  every user without a personal layout sees the new column set on
  their next changelist load.

The accompanying *Discard personal layout* / *Reset global defaults
from code* button operates on whichever scope is currently selected.

### Dynamic select_related

Pay the JOIN cost only for columns that are actually visible:

```python
class BookAdmin(DynamicColumnsMixin, admin.ModelAdmin):
    list_display_default = ["author"]
    list_display_allowed = ["publisher"]

    list_select_related = {
        "__always__": ["category"],       # always joined
        "author": ["author"],             # joined only if the column is visible
        "publisher": ["publisher"],
    }
```

## Example project

A minimal Django project demonstrating the library lives in
[`example/`](example/). Two ways to run it:

```bash
# Plain Django — SQLite, no extras:
uv pip install -e ".[dev]"
cd example
python manage.py migrate
python manage.py loaddata sample
python manage.py runserver
```

or, via [`run-site`](https://github.com/iplweb/django-run-site) +
[`django-dev-helpers`](https://pypi.org/project/django-dev-helpers/)
(Postgres + Redis testcontainers, autologin, recommended for
exploration and for LLM coding agents):

```bash
uv pip install -e .
uv pip install -r example/requirements-dev.txt
cd example
uv run --no-sync python manage.py run_site
```

`run_site` spins up the containers, migrates, creates an
`admin` / `admin` superuser and opens your browser. The
[`example/runsite.toml`](example/runsite.toml) file configures the
container names, the superuser credentials, and the welcome banner.

See [`example/README.md`](example/README.md) for the full walk-through
and the list of pre-loaded users.

## Supported versions

This package targets **actively supported** Django releases. Older
Django versions (4.2 LTS, 5.0, 5.1) are end-of-life upstream and are
not covered by CI; pin `django-dynamic-admin-columns < 0.2` if you
need to stay on them.

| Django  | 3.10 | 3.11 | 3.12 | 3.13 | 3.14 | Status                                |
|---------|------|------|------|------|------|---------------------------------------|
| 5.2 LTS | ✓    | ✓    | ✓    | ✓    | —    | Active LTS, extended support Apr 2028 |
| 6.0     | —    | —    | ✓    | ✓    | ✓    | Current mainstream                    |

CI exercises every ✓ cell on GitHub Actions.

## License

MIT. See [LICENSE](LICENSE).
