Metadata-Version: 2.4
Name: django-admin-dependent-autocomplete
Version: 0.1.0
Summary: Lightweight dependent autocomplete for Django Admin using Django's native autocomplete.
Author: django-admin-dependent-autocomplete contributors
License: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Django
Classifier: Framework :: Django :: 3.2
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django<5.3,>=3.2
Provides-Extra: dev
Requires-Dist: build; extra == "dev"
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-django; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

# django-admin-dependent-autocomplete

`django-admin-dependent-autocomplete` adds a single capability to Django
Admin's built-in autocomplete: a `ForeignKey` autocomplete can be constrained
by another `ForeignKey` in the same admin form.

It deliberately reuses Django Admin's `autocomplete_fields`, Select2 bundle,
`AutocompleteJsonView`, permissions, `search_fields`, pagination, and JSON
format. It does not introduce a second autocomplete implementation or any
runtime dependency besides Django.

## Install

```bash
pip install django-admin-dependent-autocomplete
```

Add `django_admin_dependent_autocomplete` to `INSTALLED_APPS` so Django can
discover its static JavaScript file.

## Simple shorthand dependency

```python
from django.contrib import admin
from django_admin_dependent_autocomplete.admin import DependentAutocompleteAdminMixin

from .models import Address, City, Country


@admin.register(Country)
class CountryAdmin(admin.ModelAdmin):
    search_fields = ["name"]


@admin.register(City)
class CityAdmin(admin.ModelAdmin):
    # Required by Django Admin's native autocomplete.
    search_fields = ["name"]


@admin.register(Address)
class AddressAdmin(DependentAutocompleteAdminMixin, admin.ModelAdmin):
    autocomplete_fields = ["city"]
    autocomplete_dependencies = {
        "city": "country",
    }
```

For the configuration above, `Address.city` is the autocomplete child and
`Address.country` is its parent. The related `City` model must have a
`country` `ForeignKey` to the same model. The autocomplete results are then
equivalent to:

```python
City.objects.filter(country_id=selected_country_id)
```

`search_fields` on the target model's `ModelAdmin` is required by Django
Admin for every field listed in `autocomplete_fields`.

### Target fields with a different name

The short form above uses `country` both as the parent field on `Address` and
as the `ForeignKey` on `City`. When those names differ, configure the target
model field explicitly:

```python
autocomplete_dependencies = {
    "city": {
        "depends_on": "selected_country",
        "lookup": "country",
    },
}
```

Here `selected_country` is the field on the source admin form and `country`
is the `ForeignKey` on the target autocomplete model. Both names are checked
server-side; the browser only ever supplies the selected parent primary key.

The package installs a small per-admin URL that delegates to Django's native
autocomplete view. It applies the configured parent filter before invoking the
related model admin's `get_search_results()`. Permissions, `search_fields`,
custom `get_search_results()`, limit choices, pagination, and the native JSON
response remain in use. The browser only sends a parent value; it never sends
a lookup name, and the relationship is validated server-side from the mapping.

Changing a parent clears its dependent child. Existing values on a change form
are left intact until the parent changes. When the parent is empty, its
dependent autocomplete returns no results.

## Chained dependencies

Each mapping can use an explicit target lookup, so source form fields do not
need to share names with the target model relationships. For example, a
Country → District → City chain can use intentionally different source names:

```python
class AddressAdmin(DependentAutocompleteAdminMixin, admin.ModelAdmin):
    autocomplete_fields = ["selected_district", "selected_city"]
    autocomplete_dependencies = {
        "selected_district": {
            "depends_on": "selected_country",
            "lookup": "country",
        },
        "selected_city": {
            "depends_on": "selected_district",
            "lookup": "district",
        },
    }
```

Here `Address.selected_country`, `selected_district`, and `selected_city` are
the form fields, while `District.country` and `City.district` are the target
model `ForeignKey` fields. Changing a country clears its district and city;
changing a district clears its city. Saved values remain visible on change
forms until the user changes an ancestor field.

## Mixing with a project ModelAdmin

Put `DependentAutocompleteAdminMixin` before your `ModelAdmin` subclass:

```python
class AddressAdmin(DependentAutocompleteAdminMixin, ProjectModelAdmin):
    autocomplete_fields = ["city"]
    autocomplete_dependencies = {"city": "country"}
```

The mixin uses cooperative `super()` for `formfield_for_foreignkey()`,
`get_urls()`, and `check()`. A custom `formfield_for_foreignkey()` further
down the MRO can still explicitly provide a widget; in that case it takes
precedence. Existing `get_form()` implementations are unaffected.

## Current limits

Version 0.1 supports one `ForeignKey` parent and one `ForeignKey` autocomplete
child per mapping entry. It does not support many-to-many fields, non-admin
forms, generic foreign keys, arbitrary callbacks, or multiple parents for one
child. Multiple entries can form chained dependencies. Inline formsets receive
the JavaScript behavior when added, but they are not a separately expanded API
surface yet.

## Compared with django-autocomplete-light

django-autocomplete-light is a full-featured autocomplete framework. This
package intentionally does much less: it extends Django Admin's built-in
autocomplete with dependent field filtering.

## Compatibility

The package requires Python 3.8+ and supports Django 3.2, 4.2, 5.0, 5.1, and
5.2. CI covers those Django release lines, including Python 3.8 with Django
3.2. Django is the only runtime dependency.

## Run the included test app

```bash
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
python testapp/manage.py migrate
python testapp/manage.py createsuperuser
python testapp/manage.py seed_data
python testapp/manage.py runserver
```

Open `/admin/testapp/address/add/`. Select Paraguay, then a district (Capital
or Central), then a city. Select Argentina to see its districts and cities.
Changing the country clears both dependent fields; changing a district clears
its city. Reopen a saved address to verify the selected district and city stay
visible.

## Development checks

```bash
pytest
ruff check .
python -m build
twine check dist/*
```

To run the oldest supported environment locally, install the development
dependencies with Python 3.8 and Django 3.2, then run `pytest` again.

## Roadmap

Possible future additions include multiple parent dependencies, more relation
types, and a documented inline-specific API, without changing the simple
mapping used in v0.1.
