# 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/)     |     [**๐Ÿš€ Live Demo**](https://django-baton.sqrt64.it/)     |     [Report Bug](https://github.com/otto-torino/django-baton/issues)     |     [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) ## โญ Key Features > **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. ## ๐Ÿ› ๏ธ Installation 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. ## โš™๏ธ Configuration 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 Otto srl', # HTML is safe 'POWERED_BY': 'Otto srl', # 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. ### AI Configuration 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. ### Menu Configuration 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. ### Search Field Configuration 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}) ``` ## ๐Ÿค– Baton AI In-Depth 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 ``` 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 ``` 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 {# Ensure Baton's main JS is loaded first #} ``` ### Baton AI + Editor.js [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. ## ๐Ÿ“„ Page Detection 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 #} ``` **Available Page Types:** `dashboard`, `admindocs`, `login`, `logout`, `password_change`, `password_change_success`, `add_form`, `change_form`, `changelist`, `filer`, `default`. ## ๐Ÿ“ก Signals Baton emits JavaScript events using its dispatcher. Register listeners **before** `Baton.init()`. ```html ``` **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. ## ๐Ÿงฉ JS Utilities 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: '

my html content

', // 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: '

Some HTML content for the modal body.

', size: 'lg' // Example size }); myModal.open(); // myModal.close(); // myModal.toggle(); // myModal.update({ title: 'New Modal Title', content: '

Updated content here.

' }); ``` ## ๐ŸŒ JS Translations Baton includes `en` and `it` translations for its JS messages. It detects user locale from `` 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. ## ๐Ÿ“Š List Filters ![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') ``` ## โž• Changelist Includes > > Requires browser support for HTML `