Metadata-Version: 2.4
Name: django-baton
Version: 5.4.1
Summary: A cool, modern and responsive django admin application
Home-page: http://github.com/otto-torino/django-baton
Author: abidibo
Author-email: abidibo@gmail.com
License: MIT License
Project-URL: Documentation, https://django-baton.readthedocs.io/en/latest/
Project-URL: Demo, https://django-baton.sqrt64.it/admin
Project-URL: Source, http://github.com/otto-torino/django-baton
Project-URL: Tracker, http://github.com/otto-torino/django-baton/issues
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
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
Classifier: Framework :: Django :: 2.1
Classifier: Framework :: Django :: 2.2
Classifier: Framework :: Django :: 3.0
Classifier: Framework :: Django :: 3.1
Classifier: Framework :: Django :: 4.0
Classifier: Framework :: Django :: 4.1
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Topic :: Software Development
Classifier: Topic :: Software Development :: User Interfaces
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license
Dynamic: license-file
Dynamic: project-url
Dynamic: requires-dist
Dynamic: summary

# Django Baton

[![PyPI version](https://img.shields.io/pypi/v/django-baton.svg?label=version&color=blue)](https://pypi.org/project/django-baton/)
[![Build status](https://app.travis-ci.com/otto-torino/django-baton.svg?token=fp5hqwJQgwHKLpsjsZ3L&branch=master)](https://travis-ci.com/github/otto-torino/django-baton)
[![Documentation Status](https://readthedocs.org/projects/django-baton/badge/?version=latest)](https://django-baton.readthedocs.io/en/latest/?badge=latest)
[![License](https://img.shields.io/pypi/l/django-baton)](https://github.com/otto-torino/django-baton/blob/master/LICENSE.txt)
[![Downloads](https://pepy.tech/badge/django-baton)](https://pepy.tech/project/django-baton)

**A cool, modern, responsive, and AI-enhanced Django admin interface, built on Bootstrap 5 and Material Symbols.**

[**📖 Documentation**](https://django-baton.readthedocs.io/) &nbsp;&nbsp;&nbsp; | &nbsp;&nbsp;&nbsp; [**🚀 Live Demo**](https://django-baton.sqrt64.it/) &nbsp;&nbsp;&nbsp; | &nbsp;&nbsp;&nbsp; [Report Bug](https://github.com/otto-torino/django-baton/issues) &nbsp;&nbsp;&nbsp; | &nbsp;&nbsp;&nbsp; [Request Feature](https://github.com/otto-torino/django-baton/discussions)

---

Django Baton transforms the standard Django admin into a powerful, intuitive, and visually appealing interface. Built with Bootstrap 5 and Google Material Symbols, it offers full responsiveness and integrates cutting-edge AI functionalities directly into your admin panel.

✨ **Try the Live Demo!** ✨

Experience the features of Django Baton firsthand. Login with user `demo` and password `demo`.
[**https://django-baton.sqrt64.it/**](https://django-baton.sqrt64.it/)

---

## 📣 What's New?

* **Baton 5.x:** Features a complete visual redesign and migrates from FontAwesome to Google Material Symbols for a sleek, modern icon set, see the [migration guide](https://github.com/otto-torino/django-baton/wiki/Migrate-from-v4-to-v5).
* **Baton 4.2.1:** Integrates computer vision capabilities within the `BatonAiImageField`, includes various minor styling improvements, and incorporates several community pull requests.
* **Baton 4.2.0:** Introduced computer vision for automatic generation of `alt` attributes for images.
* **Baton 4.0.\*:** Unleashed a suite of powerful AI functionalities!
  * Automatic translations (integrates with `django-modeltranslation`).
  * Text summarization for content creation.
  * Text corrections for improved writing.
  * Image generation using GPT Image.
  * This version also introduced robust theme support, making customization easier than ever. Most theme changes no longer require recompiling the JavaScript application.

> **🎨 Explore Themes!**
> Discover ready-to-use themes and get inspiration from the `django-baton-themes` repository:
> [**github.com/otto-torino/django-baton-themes**](https://github.com/otto-torino/django-baton-themes)

---

![Django Baton AI Features Showcase](docs/images/baton-ai.gif)
*An example of Baton's AI capabilities in action.*

## 📋 Table of Contents

* [Key Features](#key-features)
* [Installation](#installation)
* [Configuration](#configuration)
  * [AI Configuration](#ai-configuration)
  * [Menu Configuration](#menu-configuration)
  * [Search Field Configuration](#search-field-configuration)
* [Baton AI In-Depth](#baton-ai-in-depth)
* [Page Detection](#page-detection)
* [Signals](#signals)
* [JS Utilities](#js-utilities)
* [JS Translations](#js-translations)
* [List Filters](#list-filters)
* [Changelist Includes](#changelist-includes)
* [Changelist Filters Includes](#changelist-filters-includes)
* [Changelist Row Attributes](#changelist-row-attributes)
* [Form Tabs](#form-tabs)
* [Form Includes](#form-includes)
* [Collapsable Stacked Inlines](#collapsable-stacked-inlines)
* [Themes & Customization](#themes-customization)
* [Tests](#tests)
* [Development](#development)
* [Contributing](#contributing)
* [Star History](#star-history)

## ⭐ <a name="key-features">Key Features</a>

> **Compatibility Notes:**
>
> * For **Django >= 5.x**: Use Baton >= 5.0
> * For **5.x > Django >= 2.1**: Use Baton == 4.x
> * For **older Django versions (1.x)**: Use `django-baton==1.13.2`

Baton is designed with a core principle: **minimize overriding Django templates**. Styling is primarily achieved through CSS, with JavaScript used for dynamic functionalities.

* **Modern Stack:** Built with Bootstrap 5 and Google Material Symbols.
* **Fully Responsive:** Adapts seamlessly to all screen sizes.
* **🧠 AI Powered:**
  * Automatic translations (integrates with `django-modeltranslation`).
  * Text summarization and correction.
  * Tag suggestions for `ManyToManyField` tag fields, based on the other form fields.
  * Image vision (description generation for `alt` text).
  * Image generation with GPT Image.
  * *(Requires a Baton subscription key for AI features).*
* **Customizable Menu:** Flexible, dict-configurable sidebar navigation.
* **🎨 Theme Support:** Easily customize the look and feel.
* **Enhanced Search:** Configurable global search field with autocomplete.
* **Advanced List Filters:** Includes text input, dropdown, and multiple-choice filter options.
* **Improved Forms:**
  * Out-of-the-box tabbed interface for fieldsets and inlines.
  * Fixed submit row for better usability on long forms.
  * Collapsable entries for stacked inlines.
  * Lazy loading for uploaded images and image previews.
* **Flexible Includes:** Easily inject custom templates into changelist and change form pages.
* **Dynamic Row Attributes:** Add custom HTML attributes (classes, data-attributes, titles) to changelist rows or cells.
* **User Experience Enhancements:**
  * Optional modal display for changelist filters.
  * Optional "form mode" for changelist filters (apply multiple filters at once).
  * Confirmation for unsaved changes.
  * Loading indicator for multipart form uploads.
  * Toast notifications for admin messages.
  * Gravatar support.
* **Developer Friendly:** Customization via CSS variables or by recompiling the provided JS application for deeper changes.
* **Translations:** Includes Italian (IT) and Farsi (FA) translations.

**Frontend Technologies:**
Baton leverages Bootstrap 5 for styling and responsiveness, Google Material Symbols for icons, and jQuery for DOM manipulations. All assets are compiled into a single JavaScript file for optimized delivery.

## 🛠️ <a name="installation">Installation</a>

1. **Install via pip:**

    ```bash
    pip install django-baton
    ```

    Alternatively, to use the latest development version, clone the repository into your project:

    ```bash
    git clone [https://github.com/otto-torino/django-baton.git](https://github.com/otto-torino/django-baton.git)
    ```

2. **Add to `INSTALLED_APPS`:**
    In your project's `settings.py`, add `baton` **before** `django.contrib.admin` and `baton.autodiscover` at the **very end** of the list:

    ```python
    # settings.py
    INSTALLED_APPS = [
        # ... other apps ...
        'baton',  # Must be before django.contrib.admin
        'django.contrib.admin',
        # ... other apps ...
        'baton.autodiscover', # Must be the last app
    ]
    ```

3. **Run Migrations:**

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

4. **Update URLs:**
    Replace `django.contrib.admin` with `baton.autodiscover.admin` in your project's main `urls.py` file and include Baton's URLs:

    ```python
    # urls.py
    # from django.contrib import admin # Remove or comment out this line
    from baton.autodiscover import admin # Import Baton's admin
    from django.urls import path, include

    urlpatterns = [
        path('admin/', admin.site.urls),
        path('baton/', include('baton.urls')),
        # ... your other url patterns ...
    ]
    ```

### Why two entries in `INSTALLED_APPS`?

* `baton`: Needs to be placed *before* `django.contrib.admin` because it overrides some of Django's default admin templates and resets CSS.
* `baton.autodiscover`: This module must be the *last* app in `INSTALLED_APPS`. Baton uses a custom `AdminSite` class to allow Django-style customization of variables like `site_header` and `index_title` (instead of overriding templates). A custom `AdminSite` normally requires manual registration of all your apps. The `baton.autodiscover` module cleverly automates this by registering all apps that were already registered with Django's default `AdminSite`, ensuring all your models appear in the Baton admin. For this to work, all other apps must have already been processed.

## ⚙️ <a name="configuration">Configuration</a>

Define the `BATON` dictionary in your `settings.py` to customize various aspects of the admin interface.

```python
# settings.py
from baton.ai import AIModels # If using AI features

BATON = {
    'SITE_HEADER': 'Baton Administration',
    'SITE_TITLE': 'Baton Admin',
    'INDEX_TITLE': 'Site Administration Dashboard',
    'SUPPORT_HREF': 'https://github.com/otto-torino/django-baton/issues',
    'COPYRIGHT': 'copyright © 2025 <a href="https://www.otto.to.it">Otto srl</a>', # HTML is safe
    'POWERED_BY': '<a href="https://www.otto.to.it">Otto srl</a>', # HTML is safe
    'CONFIRM_UNSAVED_CHANGES': True,
    'SHOW_MULTIPART_UPLOADING': True,
    'ENABLE_IMAGES_PREVIEW': True,
    'CHANGELIST_FILTERS_IN_MODAL': False,
    'CHANGELIST_FILTERS_ALWAYS_OPEN': False,
    'CHANGELIST_FILTERS_FORM': False,
    'CHANGEFORM_FIXED_SUBMIT_ROW': True,
    'COLLAPSABLE_USER_AREA': True,
    'MENU_ALWAYS_COLLAPSED': False,
    'MENU_TITLE': 'Main Menu',
    'MESSAGES_TOASTS': False, # True for all, or e.g. ['warning', 'error']
    'GRAVATAR_DEFAULT_IMG': 'retro',
    'GRAVATAR_ENABLED': True,
    'LOGIN_SPLASH': '/static/core/img/login-splash.png', # Path to your login splash image
    'FORCE_THEME': None, # 'light' or 'dark', or None to allow user toggle
    'BATON_CLIENT_ID': 'your_client_id_for_ai_features',
    'BATON_CLIENT_SECRET': 'your_client_secret_for_ai_features',
    'IMAGE_PREVIEW_WIDTH': 200,
    'AI': {
        # "MODELS": "myapp.utils.get_ai_models_config", # Path to a function
        "IMAGES_MODEL": AIModels.BATON_GPT_IMAGE_1_5,
        "VISION_MODEL": AIModels.BATON_GPT_4O_MINI,
        "SUMMARIZATIONS_MODEL": AIModels.BATON_GPT_4O_MINI,
        "TRANSLATIONS_MODEL": AIModels.BATON_GPT_4O,
        'ENABLE_TRANSLATIONS': True,
        'ENABLE_CORRECTIONS': True,
        'CORRECTION_SELECTORS': [
            "textarea",
            "input[type=text]:not(.vDateField):not([name=username]):not([name*=subject_location])"
        ],
        "CORRECTIONS_MODEL": AIModels.BATON_GPT_3_5_TURBO,
    },
    'MENU': (
        { 'type': 'title', 'label': 'Main Navigation', 'apps': ('auth', ), 'icon': 'apps'},
        {
            'type': 'app',
            'name': 'auth',
            'label': 'Authentication',
            'icon': 'lock',
            'models': (
                { 'name': 'user', 'label': 'Users', 'icon': 'group' },
                { 'name': 'group', 'label': 'Groups', 'icon': 'verified_user' },
            )
        },
        { 'type': 'title', 'label': 'Content Management', 'apps': ('flatpages', ), 'icon': 'web_stories' },
        { 'type': 'model', 'label': 'Static Pages', 'name': 'flatpage', 'app': 'flatpages', 'icon': 'article' },
        { 'type': 'free', 'label': 'Custom Link', 'url': 'https://www.google.com', 'icon': 'link', 'perms': ('flatpages.add_flatpage', 'auth.change_user') },
        {
            'type': 'free',
            'label': 'Nested Menu',
            'icon': 'menu_open',
            'default_open': True,
            'children': [
                { 'type': 'model', 'label': 'A Model', 'name': 'mymodelname', 'app': 'myapp', 'icon': 'settings' },
                { 'type': 'free', 'label': 'Another Link', 'url': 'https://www.example.com', 'icon': 'public' },
            ]
        },
    )
}
```

**Detailed Configuration Options:**

* `SITE_HEADER`, `COPYRIGHT`, `POWERED_BY`: Safe for HTML content.
* `SUPPORT_HREF`: URL for a support link.
* `CONFIRM_UNSAVED_CHANGES` (Default: `True`): Prompts if leaving a dirty form. (*Note: Relies on jQuery `serialize()`, may not detect all changes.*)
* `SHOW_MULTIPART_UPLOADING` (Default: `True`): Shows spinner on multipart form submission.
* `ENABLE_IMAGES_PREVIEW` (Default: `True`): Displays image previews. Customize with `.baton-image-preview` CSS.
* `CHANGELIST_FILTERS_IN_MODAL` (Default: `False`): If `True`, filters are in a modal.
* `CHANGELIST_FILTERS_ALWAYS_OPEN` (Default: `False`): If `True` (and modal filters `False`), filters are open by default.
* `CHANGELIST_FILTERS_FORM` (Default: `False`): If `True`, treats filters as a form.
* `CHANGEFORM_FIXED_SUBMIT_ROW` (Default: `True`): Fixes submit row at the bottom.
* `COLLAPSABLE_USER_AREA`: If `True`, user area in sidebar is initially collapsed. (Check docs for default).
* `MENU_ALWAYS_COLLAPSED` (Default: `False`): If `True`, menu is collapsed by default.
* `MENU_TITLE` (Default: `'Menu'`): Sidebar menu title.
* `MESSAGES_TOASTS` (Default: `False`): Use toasts for admin messages (`True` for all, or list like `['warning', 'error']`).
* `GRAVATAR_DEFAULT_IMG` (Default: `'retro'`): Fallback Gravatar image.
* `GRAVATAR_ENABLED` (Default: `True`): Show user Gravatar.
* `LOGIN_SPLASH`: Path to login page background image.
* `FORCE_THEME` (Default: `None`): `'light'` or `'dark'` to force theme.
* `BATON_CLIENT_ID`, `BATON_CLIENT_SECRET`: Subscription keys for AI features from [baton.sqrt64.it](https://baton.sqrt64.it).
* `IMAGE_PREVIEW_WIDTH` (Default: `200`): Width (px) for `BatonAiImageField` preview.

### <a name="ai-configuration">AI Configuration</a>

Django Baton integrates AI to assist with content creation and management.

**Available Models (in `baton.ai.AIModels`):**

* `BATON_GPT_3_5_TURBO`, `BATON_GPT_4_TURBO`, `BATON_GPT_4O`: For translations, summarizations, corrections, tag suggestions.
* `BATON_GPT_4O_MINI`: Default for non-image text tasks (including tag suggestions) and image vision.
* `BATON_GPT_IMAGE_1_5`: Default for image generation.

**Configuration:**
Set preferred models in `BATON['AI']`:

```python
"AI": {
    "IMAGES_MODEL": AIModels.BATON_GPT_IMAGE_1_5,
    "VISION_MODEL": AIModels.BATON_GPT_4O_MINI,
    "TAG_SUGGESTIONS_MODEL": AIModels.BATON_GPT_4O_MINI,
    # ... etc.
}
```

Or use a function path via `"MODELS": "myapp.utils.get_ai_models_config"`.

**Translations:**
Requires `django-modeltranslation`. Enable and set model:

```python
'BATON_CLIENT_ID': 'your_client_id',
'BATON_CLIENT_SECRET': 'your_client_secret',
'AI': {
    'ENABLE_TRANSLATIONS': True,
    'TRANSLATIONS_MODEL': AIModels.BATON_GPT_4O,
    # ...
},
```

> **Note:** Review AI translations. Long text translations may require increased server timeouts.

**Corrections:**

```python
'AI': {
    'ENABLE_CORRECTIONS': True,
    'CORRECTIONS_MODEL': AIModels.BATON_GPT_4O,
    'CORRECTION_SELECTORS': [
        "textarea",
        "input[type=text]:not(.vDateField):not([name=username]):not([name*=subject_location])"
    ],
    # ...
},
```

An icon appears near fields matching selectors for corrections. Ctrl + Left Click also triggers.
![AI Corrections Screenshot](docs/images/ai-corrections.png)

**Summarizations, Tag Suggestions, Image Vision & Generation:**
Detailed in the [Baton AI In-Depth](#baton-ai-in-depth) section.

### <a name="menu-configuration">Menu Configuration</a>

Customize the sidebar via `BATON['MENU']`.

**Item Types:**

* `title`: Section header.
  * `label`, `apps` (optional), `perms` (optional), `children` (optional), `default_open` (optional), `icon` (optional Material Symbol).
* `app`: Links to a Django app.
  * `name` (lowercase app label), `label` (optional), `icon` (optional), `models` (optional tuple to customize model list), `default_open` (optional).
* `model`: Links to a model's changelist.
  * `name` (lowercase model name), `app` (lowercase app label), `label` (optional), `icon` (optional).
* `free`: Custom link.
  * `label`, `url`, `icon` (optional), `perms` (optional), `re` (optional regex for active highlighting), `children` (optional), `default_open` (optional).

> Children of items that themselves have children are ignored.

### <a name="search-field-configuration">Search Field Configuration</a>

Add an autocomplete search field to the sidebar .

![Search Field Screenshot](docs/images/search-field.png)

```python
'SEARCH_FIELD': {
    'label': 'Search contents...', # Placeholder
    'url': '/api/admin_search/',   # Your search API endpoint
}
```

Your API at `url` receives a `text` GET parameter and should return JSON:

```json
{
    "length": 1,
    "data": [
        { "label": "Search Result Label", "url": "/admin/path/to/item/", "icon": "search" }
    ]
}
```

Example Django view for the search API:

```python
# views.py
from django.http import JsonResponse
from django.contrib.admin.views.decorators import staff_member_required
# from myapp.models import YourModel # Your model

@staff_member_required
def admin_search_api(request):
    text = request.GET.get('text', None)
    response_data = []
    # Implement your search logic here
    # Example:
    # if text:
    #     items = YourModel.objects.filter(title__icontains=text)[:10]
    #     for item in items:
    #         response_data.append({
    #             'label': str(item),
    #             'url': f'/admin/myapp/yourmodel/{item.id}/change/', # Adjust URL
    #             'icon': 'article', # Material Symbol name
    #         })
    return JsonResponse({'length': len(response_data), 'data': response_data})
```

## 🤖 <a name="baton-ai-in-depth">Baton AI In-Depth</a>

AI features require `BATON_CLIENT_ID` and `BATON_CLIENT_SECRET`. See [AI Configuration](#ai-configuration) for model selection.

Baton uses `baton.sqrt64.it` APIs to generate responses, but the endpoints base path can be changed with the setting:

```python
BATON = {
    # ...
    "BATON_AI_API_BASE_PATH": "http://localhost:1323",
    # ...
}
```

In such case you should implement your own endpoints for the AI features.

### Automatic Translations

If `ENABLE_TRANSLATIONS` is `True` and `django-modeltranslation` is used, a "Translate" button appears on forms with translatable fields. Supports default fields and CKEditor. See [AI Hooks](#ai-hooks) for other editors.

### Corrections

If `ENABLE_CORRECTIONS` is `True`, an icon near text fields (matching `CORRECTION_SELECTORS`) and CKEditor fields triggers AI correction. Differences are shown in a modal.

### Text Summarization

Define `baton_summarize_fields` in your `ModelAdmin`:

```python
# admin.py
class MyModelAdmin(admin.ModelAdmin):
    # ...
    baton_summarize_fields = {
        "source_field_name_it": [{ # e.g., 'body_it'
            "target": "target_field_name_it", # e.g., 'summary_it'
            "words": 140, # Approximate
            "useBulletedList": True,
            "language": "it", # Optional, defaults to Django's current language
        },
        # ... more targets for the same source field ...
        ],
    }
```

Buttons appear near the source field to generate summaries for target fields. Parameters (`words`, `useBulletedList`) can be edited in the UI. Supports default fields and CKEditor. See [AI Hooks](#ai-hooks).

### Tag Suggestions

Define `baton_tag_suggestion_fields` in your `ModelAdmin` to let the AI suggest tags for a `ManyToManyField`, using the other fields of the same change form as context:

```python
# admin.py
class MyModelAdmin(admin.ModelAdmin):
    # ...
    baton_tag_suggestion_fields = {
        "tags": {                                  # a ManyToManyField
            "source_fields": ["title", "summary", "body"],
            "label_field": "name",
            "max_suggestions": 8,
            "allow_new": True,
        },
    }
```

Clicking the generated button sends the `source_fields` values and the existing tag labels to Baton AI. Existing tags are always preferred and returned by id, so the form selects the original related objects instead of creating duplicates. New tag candidates can be selected in the confirmation modal; when selected, Baton creates the related tag objects server-side before adding them to the form.

Optional parameters:

* `source_fields`: fields used as context for the suggestions.
* `label_field`: field used as the tag label (defaults to `name`, `title`, `label` or `slug` when available).
* `max_suggestions`: maximum number of suggestions (default `8`).
* `allow_new`: whether new tag labels can be suggested (default `True`).
* `existing_limit`: maximum number of existing tags sent as candidates (default `300`).
* `preselect_min_confidence`: existing tags are always shown, but only those whose AI confidence is at least this value are preselected in the modal (default `0.8`).

Tag suggestions always use the project default language (`MODELTRANSLATION_DEFAULT_LANGUAGE`, or the first of `LANGUAGES`); it is not configurable per field. When `django-modeltranslation` is used, `source_fields` may use base field names (Baton tries the exact field first, then the localized variants), and new tags are created with the default language label copied into all language fields. The model used can be set via `TAG_SUGGESTIONS_MODEL` (see [AI Configuration](#ai-configuration)).

### Image Generation

Use `BatonAiImageField` in your model:

```python
# models.py
from baton.fields import BatonAiImageField
from django.db import models
from django.utils.translation import gettext_lazy as _

class MyMediaModel(models.Model):
    ai_generated_image = BatonAiImageField(
        verbose_name=_("AI Generated Image"),
        upload_to="ai_images/",
        subject_location_field='image_subject_location', # Optional: for subject focus
        alt_field="image_alt_text" # Optional: for AI-generated alt text (see Image Vision)
    )
    image_subject_location = models.CharField(max_length=7, default="50,50", blank=True)
    image_alt_text = models.CharField(max_length=255, blank=True)
```

A button near the field opens a modal to generate images from text prompts.
Alternatively, for standard `ImageField`s, add generation capability with JavaScript:

```html
<script>
    Baton.AI.addImageGeneration('{{ widget.name }}'); // widget.name of the ImageField
</script>
```

Integrates `django-subject-imagefield` features for subject location. Configure preview width via `IMAGE_PREVIEW_WIDTH` in `BATON` settings.

### Image Vision

Generate `alt` text for images.

1. **Using `BatonAiImageField`**:
    Set `alt_field`, `alt_chars` (optional), `alt_language` (optional) attributes on the field. Works primarily for images within inlines.

    ```python
    # models.py
    image = BatonAiImageField(upload_to="news/", alt_field="image_alt_text", alt_chars=100)
    image_alt_text = models.CharField(max_length=150, blank=True)
    ```

2. **Using `ModelAdmin` configuration**:
    Define `baton_vision_fields` in your `ModelAdmin`:

    ```python
    # admin.py
    class MyModelAdmin(admin.ModelAdmin):
        # ...
        baton_vision_fields = {
            #id_form-0-image": [{ // CSS selector for the image field (can target inlines)
            "#id_image_field_name": [{ // Key must be a CSS selector targeting the image input or its preview
                "target": "name_of_alt_text_field", // Name of a CharField in the same model/form
                "chars": 80,                            // Optional: max characters (default 100)
                "language": "en",                       // Optional: language for description
            }],
        }
    ```

    A button appears near the specified image field. Clicking it populates the `target` field with the AI-generated description.

### Stats Widget

Display a widget showing AI feature usage statistics on your admin dashboard. Add the following to your admin index template (typically `admin/index.html` that you override):

```django
{% load baton_tags %}

{% baton_ai_stats %}
```

![Baton AI Stats Widget](docs/images/baton-ai-stats.png)

### Editor Adapters

Django Baton's AI features interact with form fields to get and set values. Native HTML inputs/textareas and fields managed by `django-ckeditor` are supported out of the box. Other rich-text editors plug in through **editor adapters**, and multiple editors can coexist on the same form (e.g. CKEditor *and* another editor).

> **Already using `dj-editor-js`?** Nothing to configure — it ships its own adapter that self-registers (see [Baton AI + Editor.js](#baton-ai-editor-js) below).

An adapter is a plain object implementing this contract:

```js
const MyEditorAdapter = {
  name: 'my-editor',
  // Return the ids of the fields this editor manages
  getFields() { return Object.keys(window.MyEditor.instances) },
  // Return the field content (string), or undefined if not owned by this editor
  getValue(fieldId) { return window.MyEditor.instances[fieldId]?.getContent() },
  // Set the content; return true if handled, false if not owned
  setValue(fieldId, value) {
    const inst = window.MyEditor.instances[fieldId]
    if (!inst) return false
    inst.setContent(value)
    return true
  },
  // Render the "correct" checkmark icon near the field; return true if handled
  setCorrect(fieldId, icon) {
    const inst = window.MyEditor.instances[fieldId]
    if (!inst) return false
    Baton.jQuery(`#${fieldId}`).after(icon)
    return true
  },
}
```

Register it in your `admin/base_site.html`, **after** `baton.min.js` and **before** `init_baton.js`:

```html
<script src="{% static 'baton/app/dist/baton.min.js' %}"></script>
<script>
  Baton.AI.registerEditorAdapter(MyEditorAdapter)
</script>
<script src="{% static 'baton/js_snippets/init_baton.js' %}"></script>
```

Registered adapters are consulted before the built-in CKEditor adapter, which is consulted last as a fallback.

### AI Hooks (legacy)

> The single-override `*Hook` functions below are **still supported** for backward compatibility, but the [editor adapter](#editor-adapters) API above is recommended for new code. When defined, the legacy hooks are wrapped as a highest-priority adapter that falls through to any registered adapters and to CKEditor, so they coexist with the adapter API.

Place these hook definitions in your `admin/base_site.html` template, **before** the `{% static 'baton/js_snippets/init_baton.js' %}` script tag:

```html
<script src="{% static 'baton/app/dist/baton.min.js' %}"></script> {# Ensure Baton's main JS is loaded first #}
<script>
(function () {
    // Hook to get a list of all field IDs managed by your custom editor.
    // Should return an array of strings (field IDs).
    Baton.AI.getEditorFieldsHook = function () {
        // Example for a hypothetical 'MyEditor':
        // if (window.MyEditor && typeof window.MyEditor.getAllInstanceIds === 'function') {
        //   return window.MyEditor.getAllInstanceIds();
        // }
        return []; // Implement for your specific editor
    };

    // Hook to get the content of a specific editor instance by its field ID.
    // Should return the string content or null/undefined if fieldId is not an editor field.
    Baton.AI.getEditorFieldValueHook = function (fieldId) {
        // Example for 'MyEditor':
        // if (window.MyEditor && typeof window.MyEditor.getInstance === 'function') {
        //   const editorInstance = window.MyEditor.getInstance(fieldId);
        //   return editorInstance ? editorInstance.getContent() : null;
        // }
        return null; // Implement for your specific editor
    };

    // Hook to set the content of a specific editor instance.
    // Should return true if the fieldId corresponds to an editor and value was set, false otherwise.
    Baton.AI.setEditorFieldValueHook = function (fieldId, value) {
        // Example for 'MyEditor':
        // if (window.MyEditor && typeof window.MyEditor.getInstance === 'function') {
        //   const editorInstance = window.MyEditor.getInstance(fieldId);
        //   if (editorInstance) {
        //     editorInstance.setContent(value);
        //     return true;
        //   }
        // }
        return false; // Implement for your specific editor
    };

    // Hook to display a "correct" icon (checkmark) near an editor field.
    // `iconElement` is a DOM element (the icon) provided by Baton.
    // Should return true if successful, false otherwise.
    Baton.AI.setEditorFieldCorrectHook = function (fieldId, iconElement) {
        // Example for 'MyEditor':
        // if (window.MyEditor && typeof window.MyEditor.getInstance === 'function') {
        //   const editorInstance = window.MyEditor.getInstance(fieldId);
        //   if (editorInstance && editorInstance.getContainer()) {
        //     // Insert iconElement after the editor's container
        //     editorInstance.getContainer().parentNode.insertBefore(iconElement, editorInstance.getContainer().nextSibling);
        //     return true;
        //   }
        // }
        return false; // Implement for your specific editor
    };
})();
</script>
<script src="{% static 'baton/js_snippets/init_baton.js' %}"></script>
```

### <a name="baton-ai-editor-js">Baton AI + Editor.js</a>

[Editor.js for Django](https://github.com/otto-torino/django-editor-js) (the `dj-editor-js` package on PyPI, version 0.2.0+) integrates with Baton AI **out of the box, zero-config**. It ships an editor adapter that self-registers on `Baton.AI`, so translation, summarization and correction work on Editor.js fields — coexisting with CKEditor and native fields on the same form.

There is nothing to add to `admin/base_site.html`: the adapter is loaded automatically via the widget's `Media`. Just make sure both apps are installed and the AI credentials are configured:

```python
INSTALLED_APPS = [
    "baton",
    "editor_js",
    # ...
    "baton.autodiscover",
]

BATON = {
    "BATON_CLIENT_ID": os.getenv("BATON_CLIENT_ID"),
    "BATON_CLIENT_SECRET": os.getenv("BATON_CLIENT_SECRET"),
    "AI": {
        "ENABLE_TRANSLATIONS": True,
        "ENABLE_CORRECTIONS": True,
    },
}
```

Then use an `EditorJSField` on your model (translatable via `django-modeltranslation`, or as a summarization target) and the AI buttons appear as for any other field. Under the hood the adapter converts Editor.js' block JSON to/from HTML so the AI sees clean prose; non-text blocks (images, tables, embeds, ...) are not sent to the AI.

## 📄 <a name="page-detection">Page Detection</a>

Baton identifies current admin page types (e.g., `change_form`, `changelist`) using regex on `location.pathname`. You can customize this for custom URLs. Define `Baton.detectPageHook` in `admin/base_site.html` **before** `init_baton.js`:

```html
{{ conf|json_script:"baton-config" }} {# Assuming conf is your BATON settings dict passed to template #}
<script src="{% static 'baton/app/dist/baton.min.js' %}"></script>
<script>
(function () {
    Baton.detectPageHook = function (defaultDetectFn) {
        if (/newschange/.test(location.pathname)) { // Example: custom URL part
            return 'change_form';
        }
        return defaultDetectFn(); // Fallback to Baton's default detection
    };
})();
</script>
<script src="{% static 'baton/js_snippets/init_baton.js' %}"></script>
```

**Available Page Types:** `dashboard`, `admindocs`, `login`, `logout`, `password_change`, `password_change_success`, `add_form`, `change_form`, `changelist`, `filer`, `default`.

## 📡 <a name="signals">Signals</a>

Baton emits JavaScript events using its dispatcher. Register listeners **before** `Baton.init()`.

```html
<script>
(function ($) { // jQuery is available as $ via Baton
    Baton.Dispatcher.register('onReady', function () { console.log('BATON IS READY'); });
    Baton.Dispatcher.register('onMenuReady', function () { console.log('BATON MENU IS READY'); });
    Baton.Dispatcher.register('onNavbarReady', function () { console.log('BATON NAVBAR IS READY'); });
    Baton.Dispatcher.register('onTabsReady', function () { console.log('BATON TABS ARE READY'); });
    Baton.Dispatcher.register('onTabChanged', function (evtName, tabData) { console.log('BATON TAB CHANGED', tabData); });
    Baton.Dispatcher.register('onMenuError', function () { console.error('BATON MENU FAILED TO LOAD'); });
})(Baton.jQuery); // Pass Baton's jQuery instance
</script>
<script src="{% static 'baton/js_snippets/init_baton.js' %}"></script>
```

**Events:**

* `onReady`: Baton JS fully initialized.
* `onNavbarReady`: Navbar rendered.
* `onMenuReady`: Menu rendered (often last, due to async fetch).
* `onTabsReady`: Form tabs rendered.
* `onTabChanged`: Active form tab changed.
* `onMenuError`: Menu content failed to load.

## 🧩 <a name="js-utilities">JS Utilities</a>

Baton exports JS modules for use in your custom admin scripts.

### Dispatcher

A singleton Mediator pattern implementation.

```javascript
// Register callback
Baton.Dispatcher.register('myCustomEvent', function (eventName, eventData) {
    console.log('Event ' + eventName + ' fired with data: ', eventData);
});

// Emit event
Baton.Dispatcher.emit('myCustomEvent', { message: 'Hello Baton!' });
```

### Modal

Create Bootstrap modals programmatically.
![Modal Screenshot](docs/images/modals.png)

```javascript
// Example Modal Configuration Object:
// let config = {
//     title: 'My modal title',
//     subtitle: 'My subtitle', // optional
//     content: '<p>my html content</p>', // alternative to url
//     url: '/my/url', // fetches content via AJAX; alternative to content.
//     hideFooter: false, // optional
//     showBackBtn: false, // optional, show a back button
//     backBtnCb: function () {}, // optional, back button click callback
//     actionBtnLabel: 'save', // optional, default 'save'
//     actionBtnCb: null, // optional, action button callback
//     onUrlLoaded: function () {}, // optional, callback after AJAX content loads
//     size: 'lg', // optional: sm, md, lg, xl
//     onClose: function () {} // optional, callback when modal closes
// };

let myModal = new Baton.Modal({
    title: 'My Modal Title',
    content: '<p>Some HTML content for the modal body.</p>',
    size: 'lg' // Example size
});

myModal.open();
// myModal.close();
// myModal.toggle();
// myModal.update({ title: 'New Modal Title', content: '<p>Updated content here.</p>' });
```

## 🌐 <a name="js-translations">JS Translations</a>

Baton includes `en` and `it` translations for its JS messages. It detects user locale from `<html>` tag's `lang` attribute. Add/override translations by defining `Baton.translations` **before** `Baton.init()`:

```javascript
// Place in admin/base_site.html before init_baton.js
Baton.translations = {
  // Default English, override or add other locales
  en: {
    unsavedChangesAlert: 'You have some unsaved changes.',
    uploading: 'Uploading...',
    filter: 'Filter',
    close: 'Close',
    save: 'Save',
    search: 'Search',
    cannotCopyToClipboardMessage: 'Cannot copy to clipboard, please do it manually: Ctrl+C, Enter',
    retrieveDataError: 'There was an error retrieving the data',
    lightTheme: 'Light theme',
    darkTheme: 'Dark theme'
  },
  it: { // Example for Italian
    unsavedChangesAlert: 'Ci sono modifiche non salvate.',
    uploading: 'Caricamento...',
    // ... other Italian translations
  }
  // Add other locales as needed, e.g. 'es': { ... }
};
```

Baton defaults to `en` if a translation for the user's locale is not found.

## 📊 <a name="list-filters">List Filters</a>

![List Filters Screenshot](docs/images/filters.png)

### Input Text Filters

Create text input filters in your `ModelAdmin`. (Adapted from [this article](https://medium.com/@hakibenita/how-to-add-a-text-filter-to-django-admin-5d1db93772d8)).

```python
# admin.py
from baton.admin import InputFilter
from django.contrib import admin # If not already imported

class MyModelIdFilter(InputFilter):
    parameter_name = 'id' # URL query parameter
    title = 'ID'          # Display title for the filter

    def queryset(self, request, queryset):
        if self.value() is not None:
            # Ensure value is treated as expected type, e.g., int for ID
            try:
                search_term = int(self.value())
                return queryset.filter(id=search_term)
            except ValueError:
                return queryset.none() # Or handle error appropriately
        return queryset

class MyModelAdmin(admin.ModelAdmin):
    list_display = ('id', 'name', 'other_field') # Example
    list_filter = (MyModelIdFilter, 'other_field')
```

### Dropdown Filters

Provides dropdown versions of standard Django admin list filters if a filter has at least 3 options. (Inspired by `django-admin-list-filter-dropdown`).

| Django Admin Filter      | Baton Equivalent            |
| :----------------------- | :-------------------------- |
| `SimpleListFilter`       | `SimpleDropdownFilter`      |
| `AllValuesFieldListFilter` | `DropdownFilter`            |
| `ChoicesFieldListFilter` | `ChoicesDropdownFilter`     |
| `RelatedFieldListFilter` | `RelatedDropdownFilter`     |
| `RelatedOnlyFieldListFilter`| `RelatedOnlyDropdownFilter` |

Usage:

```python
# admin.py
from baton.admin import DropdownFilter, RelatedDropdownFilter, ChoicesDropdownFilter
# from myapp.models import MyModel, MyRelatedModel # Your models

class MyModelAdmin(admin.ModelAdmin):
    # list_display = ('name', 'char_field', 'choice_field', 'foreign_key_field') # Example
    list_filter = (
        ('char_field', DropdownFilter), # For CharField, TextField etc.
        ('choice_field', ChoicesDropdownFilter), # For fields with choices
        ('foreign_key_field', RelatedDropdownFilter), # For ForeignKey, ManyToManyField
    )
```

### Multiple Choice Filters

Filter on multiple options for a field.

```python
# admin.py
from baton.admin import MultipleChoiceListFilter
# from myapp.models import News # Assuming News model with Status choices

class NewsStatusListFilter(MultipleChoiceListFilter):
    title = 'Status'
    parameter_name = 'status__in' # Query parameter for __in lookup

    def lookups(self, request, model_admin):
        # Example assuming News.Status has .choices attribute
        # return News.Status.choices
        return (('draft', 'Draft'), ('published', 'Published'), ('archived', 'Archived')) # Example choices

class NewsAdmin(admin.ModelAdmin):
    # list_display = ('title', 'status') # Example
    list_filter = (NewsStatusListFilter, 'publication_date')
```

## ➕ <a name="changelist-includes">Changelist Includes</a>
>
> Requires browser support for HTML `<template>` tags.

Embed custom templates within the changelist page.

```python
# admin.py
# from django.contrib import admin # If using @admin.register
# from myapp.models import News # Your model

# @admin.register(News)
class NewsAdmin(admin.ModelAdmin):
    # ...
    baton_cl_includes = [
        ('myapp/admin_includes/cl_top_banner.html', 'top'),
        ('myapp/admin_includes/cl_below_table.html', 'below'),
    ]
```

![Changelist Includes Screenshot](docs/images/baton-cl-includes.png)

**Positions:**

| Position | Description                                     |
| :------- | :---------------------------------------------- |
| `top`    | Inside changelist form, at the top.             |
| `bottom` | Inside changelist form, at the bottom.          |
| `above`  | Above the entire changelist form.               |
| `below`  | Below the entire changelist form.               |

Changelist view context variables are available in your included template.

**Object Tools Includes:**
Inject templates into the object tools bar (top right of changelist). Templates are injected inside a `<ul>`.

```python
# admin.py
class NewsAdmin(admin.ModelAdmin):
    # ...
    baton_cl_object_tools_include = ('myapp/admin_includes/custom_cl_action.html', 'left') # or 'right'
```

## ☰ <a name="changelist-filters-includes">Changelist Filters Includes</a>
>
> Requires browser support for HTML `<template>` tags.

Embed custom templates within the changelist filter container.

```python
# admin.py
class NewsAdmin(admin.ModelAdmin):
    # ...
    baton_cl_filters_includes = [
        ('myapp/admin_includes/filters_top_custom_filter.html', 'top'),
        ('myapp/admin_includes/filters_bottom_info.html', 'bottom'),
    ]
```

![Changelist Filters Includes Screenshot](docs/images/baton-cl-filters-includes.png)

**Positions:**

| Position | Description                                          |
| :------- | :--------------------------------------------------- |
| `top`    | Inside filter container, at the top.                 |
| `bottom` | Inside filter container, at the bottom.              |

Changelist view context variables are available.

## ↔️ <a name="changelist-row-attributes">Changelist Row Attributes</a>
>
> Requires browser support for HTML `<template>` tags.

Add HTML attributes (classes, `data-*`, `title`, etc.) to elements in the changelist table (rows, cells).
![Changelist Row Attributes Screenshot](docs/images/baton-cl-row-attributes.png)

1. Define `baton_cl_rows_attributes` method in your `ModelAdmin`. It takes `request` and `cl` (changelist instance) as arguments.
2. Return a JSON string dictionary. Keys usually match instance IDs. Values are dicts specifying attributes and selectors.

```python
# admin.py
import json
from django.utils.safestring import mark_safe
# from myapp.models import News # Assuming News model

class NewsAdmin(admin.ModelAdmin):
    list_display = ('title', 'get_category_display', 'status') # Use the method name

    def get_category_display(self, instance):
        # Helper for targeting specific cells if needed by selector
        if instance.category: # Check if category exists
            return mark_safe(f'<span class="category-span-{instance.category.id}">{instance.category.name}</span>')
        return "-" # Fallback if no category
    get_category_display.short_description = 'Category'
    get_category_display.admin_order_field = 'category' # Optional: if you want to allow ordering

    def baton_cl_rows_attributes(self, request, cl):
        data = {}
        # Example 1: Add 'table-info' class to rows of news items in category ID 2
        for news_item in cl.queryset.filter(category__id=2):
            data[str(news_item.id)] = { # Ensure key is string for JSON
                'class': 'table-info',
            }

        # Example 2: More complex - target a specific cell for a specific news item
        # This example assumes you want to style a cell for a news item with ID=1 and category_id=1
        try:
            news_to_style = cl.queryset.get(id=1, category__id=1) # More specific lookup
            data[f"customkey_cell_{news_to_style.id}"] = { # Key can be arbitrary if selector is specific
                'class': 'table-success font-weight-bold', # Example: bold success
                'data-category-name': news_to_style.category.name if news_to_style.category else '',
                'title': f'Special: {news_to_style.title}',
                # This selector targets the span created by get_category_display
                # It assumes the changelist renders the output of get_category_display in a cell
                'selector': f'#result_list tr input[name=_selected_action][value="{news_to_style.pk}"] ~ td .category-span-{news_to_style.category_id}',
                'getParent': 'td', # Applies attributes to the parent <td> of the found span
            }
        except cl.model.DoesNotExist: # Or your specific model DoesNotExist
            pass # Item not found, or doesn't match criteria

        return json.dumps(data)
```

**Rules for the returned dictionary values:**

* **Keys:** Typically the primary key of the model instance (as a string). If using a custom `selector` that doesn't rely on the instance ID, the key can be any unique string.
* **`selector`** (optional): CSS selector to find the target element.
  * Default: `'#result_list tr input[name=_selected_action][value="' + key + '"]'` (targets the checkbox for the row of instance `key`). This works if `actions` are enabled.
* **`getParent`** (optional):
  * Default: `'tr'` (attributes are applied to the row).
  * You can specify another selector (e.g., `'td'`, `'.field-my_field'`) to find a parent of the element matched by `selector`.
  * Set to `false` (boolean, not string) or an empty string to apply attributes directly to the element matched by `selector`.
* **Other keys:** Treated as HTML attributes to be added to the target element.

## 📑 <a name="form-tabs">Form Tabs</a>

![Form Tabs Screenshot](docs/images/tabs.png)
Organize your admin forms with tabs for fieldsets and inlines. Titles are derived automatically.

**Configuration (in `ModelAdmin.fieldsets` or `ModelAdmin.inlines`):**

```python
# admin.py
# from myapp.models import Attribute, Feature # Your models

# class AttributeInline(admin.StackedInline):
#     model = Attribute # Your model
#     extra = 1

# class FeatureInline(admin.StackedInline):
#     model = Feature # Your model
#     extra = 1

class ItemAdmin(admin.ModelAdmin):
    # list_display = ('label', 'description', 'main_feature')
    # inlines = [AttributeInline, FeatureInline] # Order of inlines matters for grouping

    fieldsets = (
        ('Main Info', { # This fieldset will be the first tab (or part of it)
            'fields': ('label', 'description'),
            'classes': ('baton-tabs-init', 'order-0', 'baton-tab-group-main--inline-attribute'),
            # 'baton-tabs-init': REQUIRED on the first fieldset to enable tabs.
            # 'order-X': (Optional) Defines the tab order for this fieldset. Default 0.
            # 'baton-tab-inline-MODELNAME': Creates a tab for the inline 'attribute' (lowercase model name).
            # 'baton-tab-fs-CUSTOMNAME': Creates a tab for this fieldset (Main Info -> content_tab).
            # 'baton-tab-group-GROUPNAME--item1type-ITEMNAME--item2type-ITEMNAME': Creates a group tab.
            #    GROUPNAME is arbitrary. ITEMNAME can be fs-FIELDSETNAME or inline-INLINEMODELNAME.
            #    Example: 'baton-tab-group-overview--fs-main_info--inline-attribute'
            #    This creates a group tab named "Overview" containing the "Main Info" fieldset and the "Attribute" inline.
            'description': 'This is the main information for the item.'
        }),
        ('Content Details', {
            'fields': ('text', ),
            'classes': ('baton-tab-fs-content', ), # This fieldset becomes a tab named "Content"
            'description': 'Detailed content for the item.'
        }),
        ('Technical Specs', {
            'fields': ('main_feature', ),
            # This fieldset is part of a group tab defined in the "Main Info" fieldset:
            # e.g. 'baton-tab-group-main--inline-attribute--fs-tech--inline-feature' in 'Main Info' would group this.
            'classes': ('baton-tab-fs-tech', ),
            'description': 'Technical specifications and features.'
        }),
    )
```

**Rules for Tab Classes (applied to a fieldset's `classes` tuple):**

* **`baton-tabs-init`**: **Required** on the *first* fieldset definition to activate the tabbing system.
* **`order-X`**: (Optional, on the first fieldset) Sets the display order of the tab generated by the first fieldset itself. `X` is a number (e.g., `order-0`, `order-1`).
* **`baton-tab-inline-MODELNAME`**: Creates a separate tab for the inline whose model is `MODELNAME` (lowercase). If you used `related_name` for the inline, use `baton-tab-inline-RELATEDNAME`.
* **`baton-tab-fs-CUSTOMNAME`**: Creates a separate tab for the fieldset that *also* has the class `tab-fs-CUSTOMNAME`. `CUSTOMNAME` is an arbitrary name you choose.
* **`baton-tab-group-GROUPNAME--item1type-ITEM1NAME--item2type-ITEM2NAME...`**: Creates a group tab.
  * `GROUPNAME` is an arbitrary name for your tab.
  * `itemXtype` is either `fs` (for fieldset) or `inline`.
  * `ITEMXNAME` is your `CUSTOMNAME` (for fieldsets) or `MODELNAME`/`RELATEDNAME` (for inlines).
  * Example: `baton-tab-group-overview--fs-main_content--inline-attributes`
* Fieldsets without a `baton-tab-fs-*` class that are *not* part of a group will be appended to the first tab.
* To make a fieldset *always visible* (not part of any tab), add the class `tab-fs-none` to that fieldset.

**Other Tab Features:**

* If a form field has an error, the first tab containing that field is automatically opened.
* Deep link to a tab by adding its hash to the URL (e.g., `#inline-feature`, `#fs-content`, `#group-overview--fs-main_content--inline-attributes`). The hash is derived from the tab class names.

## 📎 <a name="form-includes">Form Includes</a>
>
> Requires browser support for HTML `<template>` tags.

Embed custom templates near specific fields in change forms.

```python
# admin.py
class NewsAdmin(admin.ModelAdmin):
    # ...
    baton_form_includes = [
        ('myapp/admin_includes/datetime_helper.html', 'publication_date', 'top'),
        ('myapp/admin_includes/content_notes.html', 'body_content', 'above'),
        ('myapp/admin_includes/field_icon.html', 'title', 'right'),
    ]
```

![Form Includes Screenshot](docs/images/baton-form-includes.png)

**Positions:**

| Position | Description                                  |
| :------- | :------------------------------------------- |
| `top`    | Inside the field's form row, at the top.     |
| `bottom` | Inside the field's form row, at the bottom.  |
| `above`  | Above the field's form row.                  |
| `below`  | Below the field's form row.                  |
| `right`  | Inline, to the right of the input field.     |

The `{{ original }}` object (the model instance) is available in your included template. Works with tabs.

**Object Tools Includes:**
Inject templates into the object tools bar (top right of change form). Templates are injected inside a `<ul>`.

```python
# admin.py
class NewsAdmin(admin.ModelAdmin):
    # ...
    baton_form_object_tools_include = ('myapp/admin_includes/custom_object_action.html', 'left') # or 'right'
```

![Form Object Tools Includes Screenshot](docs/images/baton_form_object_tools_include.png)

## 🤏 <a name="collapsable-stacked-inlines">Collapsable Stacked Inlines</a>

![Collapsable Stacked Inlines Screenshot](docs/images/collapsable_stackedinline.png)
Make individual entries in `admin.StackedInline` collapsable.

Add `collapse-entry` to the inline's `classes`:

```python
# admin.py
class VideoInline(admin.StackedInline):
    # model = Video # Your model
    extra = 1
    classes = ('collapse-entry', ) # Can be combined with Django's 'collapse'
```

To have the first entry expanded by default:

```python
# admin.py
class VideoInline(admin.StackedInline):
    # model = Video # Your model
    extra = 1
    classes = ('collapse-entry', 'expand-first')
```

## 🎨 <a name="themes-customization">Themes & Customization</a>

Easily customize Baton's appearance:

1. **CSS Variables:**
    Create a `baton/css/root.css` file in one of your app's static directories (ensure this app is listed *before* `baton` in `INSTALLED_APPS`). Override any CSS variables defined in Baton's default [root.css](https://github.com/otto-torino/django-baton/tree/master/baton/static/baton/css/root.css).
    Example:

    ```css
    /* myapp/static/baton/css/root.css */
    :root {
      --bs-primary: #FF6347;
      --bs-primary-rgb: 255,99,71;
      --baton-sidebar-active-bg: #FF6347;
    }
    ```

2. **Admin Themes:**
    Create and manage themes directly from the admin site at `/admin/baton/batontheme/`. Only one theme can be active. Its CSS content (which should define CSS variables) will override the `baton/css/root.css` file.
    > **Caution:** Theme content is marked safe and injected as-is. Be careful.
    > ✨ Find ready-to-use themes at [django-baton-themes](https://github.com/otto-torino/django-baton-themes).

3. **Heavy Customization (Recompiling JS App):**
    For changes to primary/secondary Bootstrap colors or extensive modifications, you can recompile Baton's JavaScript application.
    ![Customization Screenshot](docs/images/customization.png)
    1. Clone `django-baton`.
    2. Navigate to `django-baton/baton/static/baton/app/`.
    3. Run `npm install`.
    4. Edit `src/styles/_variables.scss` (and other SCSS/JS files as needed).
    5. Run `npm run compile`.
    6. Copy the compiled `dist/baton.min.js` to your project: `YOUR_APP/static/baton/app/dist/`.
    7. Ensure `YOUR_APP` is listed *before* `baton` in `INSTALLED_APPS`.

    For live development with automatic recompilation:
    1. `cd django-baton/baton/static/baton/app/`
    2. Run `npm run dev:baton` (starts Webpack dev server, usually on `http://localhost:8080`).
    3. In your project's `admin/base_site.html` (you might need to override it), change the script source to point to the dev server:

        ```html
        {# <script src="{% static 'baton/app/dist/baton.min.js' %}"></script> #}
        <script src="http://localhost:8080/static/baton/app/dist/baton.min.js"></script>
        ```

    Now, changes in the JS app will auto-update, just refresh your Django admin page.

## 🧪 <a name="tests">Tests</a>

Baton includes unit and end-to-end (e2e) tests using Selenium. To run e2e tests, ensure the test application (found in the `testapp` directory of the Baton repository) is running on `localhost:8000`.

## 💻 <a name="development">Development</a>

To contribute or develop locally:

1. **Set up the test app:**

    ```bash
    cd testapp
    python3 -m venv .virtualenv
    source .virtualenv/bin/activate # On Windows: .virtualenv\Scripts\activate
    cd app
    pip install -r requirements.txt
    python manage.py migrate
    python manage.py createsuperuser # If needed
    python manage.py runserver
    ```

    (Default login after `createsuperuser`: `admin` / `admin`, or as you defined).

2. **Enable live JS recompilation for development:**
    * In `testapp/app/templates/admin/base_site.html`, switch the script source to Webpack dev server:

        ```html
        {# <script src="{% static 'baton/app/dist/baton.min.js' %}"></script> #}
        <script src="http://localhost:8080/static/baton/app/dist/baton.min.js"></script>
        ```

    * In a new terminal, navigate to Baton's frontend app directory and start the dev server:

        ```bash
        cd /path/to/your/django-baton/baton/static/baton/app/
        npm install
        npm run dev # For continuous development, watches for changes
        ```

    Changes to Baton's frontend app will now auto-recompile. Refresh your browser to see them.

### Commands

Install `invoke` and `sphinx_rtd_theme` in your Python environment for documentation generation:

```bash
pip install invoke sphinx_rtd_theme
```

To generate documentation locally (from the root directory of the `django-baton` repository):

```bash
invoke docs
```

## 🤝 <a name="contributing">Contributing</a>

Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. We welcome contributions!

## 🌟 <a name="star-history">Star History</a>

[![Star History Chart](https://api.star-history.com/svg?repos=otto-torino/django-baton&type=Date)](https://star-history.com/#otto-torino/django-baton&Date)
