Metadata-Version: 2.4
Name: django-admin-dependent-autocomplete
Version: 0.2.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, State


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


@admin.register(State)
class StateAdmin(admin.ModelAdmin):
    search_fields = ["name"]


@admin.register(City)
class CityAdmin(admin.ModelAdmin):
    search_fields = ["name"]


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

For the configuration above, `Address.state` depends on `Address.country`, and
`Address.city` depends on `Address.state`. The related models use matching
ForeignKey relationships:

```python
State.objects.filter(country_id=country_id)
City.objects.filter(state_id=state_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 lookup name

The short form above uses the same field name on the source form and on the
target model. When those names differ, configure the target model field
explicitly:

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

Here `state` is the field on the source admin form and `lookup` names the
`ForeignKey` on the target autocomplete model when validation needs to be
explicit. 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.

## Inline dependencies

Version 0.2 adds formal support for dependent autocompletes inside direct
inlines. Both the hosting `ModelAdmin` and the inline class must use
`DependentAutocompleteAdminMixin`.

### Same inline row

When `source` is omitted, the parent field is resolved in the same inline row
or main form field prefix:

```python
autocomplete_dependencies = {
    "city": "state",
}
```

### Parent form source

Use `source="parent"` when the parent ForeignKey lives on the hosting
ModelAdmin change form rather than in the inline row:

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

from .models import City, Country, Person, PersonLocation, State


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


@admin.register(State)
class StateAdmin(admin.ModelAdmin):
    search_fields = ["name"]


@admin.register(City)
class CityAdmin(admin.ModelAdmin):
    search_fields = ["name"]


class PersonLocationInline(DependentAutocompleteAdminMixin, admin.TabularInline):
    model = PersonLocation
    autocomplete_fields = ["state", "city", "backup_city"]
    autocomplete_dependencies = {
        "state": {
            "depends_on": "country",
            "lookup": "country",
            "source": "parent",
        },
        "city": "state",
    }


@admin.register(Person)
class PersonAdmin(DependentAutocompleteAdminMixin, admin.ModelAdmin):
    autocomplete_fields = ["country"]
    inlines = [PersonLocationInline]
```

| Configuration | Parent field location | Child field location |
|---------------|----------------------|----------------------|
| `"city": "state"` | Same inline row | Inline row |
| `source="parent"` | Hosting ModelAdmin form | Inline row |

Requirements and limits:

- `source="parent"` is only valid on direct inlines, not on the hosting ModelAdmin itself.
- Nested inline parent dependencies are not supported.
- The browser still sends only `dependent_parent=<pk>`; lookup and config remain server-owned.
- Normal `autocomplete_fields` without a dependency mapping continue to use Django's native autocomplete endpoint.
- `TabularInline` and `StackedInline` are supported through the same mixin.

The inline model does not need its own `ModelAdmin` registration. The hosting
ModelAdmin registers one dependent-autocomplete URL per inline class that defines
`autocomplete_dependencies`.

## Chained dependencies

The bundled test app demonstrates a Country → State → City chain on
`AddressAdmin`:

```python
class AddressAdmin(DependentAutocompleteAdminMixin, admin.ModelAdmin):
    autocomplete_fields = ["state", "city", "backup_city"]
    autocomplete_dependencies = {
        "state": "country",
        "city": "state",
    }
```

Here `Address.country`, `state`, and `city` are the form fields, while
`State.country` and `City.state` are the target model `ForeignKey` fields.
Changing a country clears its state and city; changing a state clears its city.
Saved values remain visible on change forms until the user changes an ancestor
field. `backup_city` stays a normal autocomplete with no dependency mapping.

## Mixing with a project ModelAdmin

Put `DependentAutocompleteAdminMixin` before your `ModelAdmin` subclass:

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

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.2 supports one `ForeignKey` parent and one `ForeignKey` autocomplete
child per mapping entry on main forms and direct inlines. It does not support
many-to-many fields, non-admin forms, generic foreign keys, arbitrary callbacks,
multiple parents for one child, nested inline parent dependencies, or sibling
inline dependencies. Multiple entries can form chained dependencies within the
same form or inline row.

## 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. The package uses Django Admin's
native `AutocompleteSelect` widgets and remains compatible with admin themes
that preserve those widgets.

## 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/` for the same-form Country → State → City
demo, or `/admin/testapp/person/add/` for the inline parent-form demo with the
same United States / Canada geography.

### Manual smoke test for inline dependencies

On `/admin/testapp/person/add/` or change:

- Add multiple inline rows and verify each row filters state by the selected country.
- Change the country and confirm dependent state and city values clear; `backup_city` stays unchanged.
- Add a dynamic inline row and verify dependent autocompletes still filter correctly.
- Reopen a saved person and confirm saved state and city remain visible until a parent changes.

## 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 nested inline parent dependencies, without changing the simple
mapping used in v0.2.
