{"id":13415508,"url":"https://github.com/otto-torino/django-baton","last_synced_at":"2026-01-16T10:42:12.190Z","repository":{"id":37502762,"uuid":"81565159","full_name":"otto-torino/django-baton","owner":"otto-torino","description":"A cool, modern and responsive django admin application based on bootstrap 5 that brings AI to the Django admin - https://baton.sqrt64.it/","archived":false,"fork":false,"pushed_at":"2025-07-08T15:34:45.000Z","size":69452,"stargazers_count":974,"open_issues_count":6,"forks_count":98,"subscribers_count":14,"default_branch":"master","last_synced_at":"2025-10-31T13:28:52.343Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/otto-torino.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2017-02-10T12:54:24.000Z","updated_at":"2025-10-15T13:56:48.000Z","dependencies_parsed_at":"2026-01-08T16:05:22.813Z","dependency_job_id":null,"html_url":"https://github.com/otto-torino/django-baton","commit_stats":{"total_commits":541,"total_committers":11,"mean_commits":49.18181818181818,"dds":"0.044362292051756014","last_synced_commit":"42f4e64de255f5a6d35a0d4a06dff7dfb2a97429"},"previous_names":[],"tags_count":121,"template":false,"template_full_name":null,"purl":"pkg:github/otto-torino/django-baton","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/otto-torino%2Fdjango-baton","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/otto-torino%2Fdjango-baton/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/otto-torino%2Fdjango-baton/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/otto-torino%2Fdjango-baton/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/otto-torino","download_url":"https://codeload.github.com/otto-torino/django-baton/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/otto-torino%2Fdjango-baton/sbom","scorecard":{"id":714507,"data":{"date":"2025-08-11","repo":{"name":"github.com/otto-torino/django-baton","commit":"a3f22f8e8661768d6085954c8eeadcb1c817a51e"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.7,"checks":[{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/python-publish.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":10,"reason":"30 commit(s) and 4 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":3,"reason":"branch protection is not maximal on development and all release branches","details":["Info: 'allow deletion' disabled on branch 'master'","Info: 'force pushes' disabled on branch 'master'","Info: 'branch protection settings apply to administrators' is required to merge on branch 'master'","Warn: branch 'master' does not require approvers","Warn: codeowners review is not required on branch 'master'","Warn: no status checks found to merge onto branch 'master'","Warn: PRs are not required to make changes on branch 'master'; or we don't have data to detect it.If you think it might be the latter, make sure to run Scorecard with a PAT or use Repo Rules (that are always public) instead of Branch Protection settings"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/python-publish.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/otto-torino/django-baton/python-publish.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/python-publish.yml:16: update your workflow using https://app.stepsecurity.io/secureworkflow/otto-torino/django-baton/python-publish.yml/master?enable=pin","Warn: pipCommand not pinned by hash: .github/workflows/python-publish.yml:21","Warn: pipCommand not pinned by hash: .github/workflows/python-publish.yml:22","Info:   0 out of   2 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   2 pipCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Vulnerabilities","score":0,"reason":"48 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-67hx-6x53-jw92","Warn: Project is vulnerable to: GHSA-qwcr-r2fm-qrc7","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-cwfw-4gq5-mrqx","Warn: Project is vulnerable to: GHSA-g95f-p29q-9xw4","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-pxg6-pf52-xh8x","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-qw6h-vgh9-j6wx","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-c7qv-q95q-8v27","Warn: Project is vulnerable to: GHSA-2p57-rm9w-gvfp","Warn: Project is vulnerable to: GHSA-9c47-m6qq-7p4h","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-mwcw-c2x4-8c55","Warn: Project is vulnerable to: GHSA-5rrq-pxf6-6jx5","Warn: Project is vulnerable to: GHSA-8fr3-hfg3-gpgp","Warn: Project is vulnerable to: GHSA-gf8q-jrpm-jvxq","Warn: Project is vulnerable to: GHSA-2r2c-g63r-vccr","Warn: Project is vulnerable to: GHSA-cfm4-qjh2-4765","Warn: Project is vulnerable to: GHSA-x4jg-mjrx-434g","Warn: Project is vulnerable to: GHSA-r8f7-9pfq-mjmv","Warn: Project is vulnerable to: GHSA-76c9-3jph-rj3q","Warn: Project is vulnerable to: GHSA-9wv6-86v2-598j","Warn: Project is vulnerable to: GHSA-rhx6-c78j-4q9w","Warn: Project is vulnerable to: GHSA-p8p7-x288-28g6","Warn: Project is vulnerable to: GHSA-7mwh-4pqv-wmr8","Warn: Project is vulnerable to: GHSA-m6fv-jmcg-4jfg","Warn: Project is vulnerable to: GHSA-cm22-4g7w-348p","Warn: Project is vulnerable to: GHSA-72xf-g2v4-qvf3","Warn: Project is vulnerable to: GHSA-4vvj-4cpr-p986","Warn: Project is vulnerable to: GHSA-wr3j-pwj9-hqq6","Warn: Project is vulnerable to: GHSA-4v9v-hfq4-rm2v","Warn: Project is vulnerable to: GHSA-9jgg-88mc-972h","Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q","Warn: Project is vulnerable to: GHSA-79v4-65xg-pq4g","Warn: Project is vulnerable to: PYSEC-2025-47 / GHSA-7xr5-9hcq-chf9","Warn: Project is vulnerable to: GHSA-wjrh-hj83-3wh7","Warn: Project is vulnerable to: GHSA-vqfr-h8mv-ghfj","Warn: Project is vulnerable to: GHSA-cpwx-vrp4-4pq7","Warn: Project is vulnerable to: GHSA-gmj6-6f8f-6699","Warn: Project is vulnerable to: GHSA-q2x7-8rv6-6q7h","Warn: Project is vulnerable to: GHSA-8qvm-5x2c-j2w7","Warn: Project is vulnerable to: GHSA-9hjg-9r4m-mvj7","Warn: Project is vulnerable to: PYSEC-2025-49 / GHSA-5rjg-fvgr-3xxf","Warn: Project is vulnerable to: GHSA-48p4-8xcf-vxj5","Warn: Project is vulnerable to: GHSA-pq67-6m6q-mj2v"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-22T09:10:16.291Z","repository_id":37502762,"created_at":"2025-08-22T09:10:16.291Z","updated_at":"2025-08-22T09:10:16.291Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28478050,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-16T06:30:42.265Z","status":"ssl_error","status_checked_at":"2026-01-16T06:30:16.248Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-30T21:00:49.871Z","updated_at":"2026-01-16T10:42:12.174Z","avatar_url":"https://github.com/otto-torino.png","language":"Python","funding_links":[],"categories":["Third-Party Packages","Python","Best Django Admin Interface Resources","Packages to install at your project","Django Admin Themes"],"sub_categories":["Admin","Admin Themes","Django Admin Themes \u0026 UI Enhancements","Alternative Themes"],"readme":"# Django Baton\n\n[![PyPI version](https://img.shields.io/pypi/v/django-baton.svg?label=version\u0026color=blue)](https://pypi.org/project/django-baton/)\n[![Build status](https://app.travis-ci.com/otto-torino/django-baton.svg?token=fp5hqwJQgwHKLpsjsZ3L\u0026branch=master)](https://travis-ci.com/github/otto-torino/django-baton)\n[![Documentation Status](https://readthedocs.org/projects/django-baton/badge/?version=latest)](https://django-baton.readthedocs.io/en/latest/?badge=latest)\n[![License](https://img.shields.io/pypi/l/django-baton)](https://github.com/otto-torino/django-baton/blob/master/LICENSE.txt)\n[![Downloads](https://pepy.tech/badge/django-baton)](https://pepy.tech/project/django-baton)\n\n**A cool, modern, responsive, and AI-enhanced Django admin interface, built on Bootstrap 5 and Material Symbols.**\n\n[**📖 Documentation**](https://django-baton.readthedocs.io/) \u0026nbsp;\u0026nbsp;\u0026nbsp; | \u0026nbsp;\u0026nbsp;\u0026nbsp; [**🚀 Live Demo**](https://django-baton.sqrt64.it/) \u0026nbsp;\u0026nbsp;\u0026nbsp; | \u0026nbsp;\u0026nbsp;\u0026nbsp; [Report Bug](https://github.com/otto-torino/django-baton/issues) \u0026nbsp;\u0026nbsp;\u0026nbsp; | \u0026nbsp;\u0026nbsp;\u0026nbsp; [Request Feature](https://github.com/otto-torino/django-baton/discussions)\n\n---\n\nDjango 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.\n\n✨ **Try the Live Demo!** ✨\n\nExperience the features of Django Baton firsthand. Login with user `demo` and password `demo`.\n[**https://django-baton.sqrt64.it/**](https://django-baton.sqrt64.it/)\n\n---\n\n## 📣 What's New?\n\n* **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).\n* **Baton 4.2.1:** Integrates computer vision capabilities within the `BatonAiImageField`, includes various minor styling improvements, and incorporates several community pull requests.\n* **Baton 4.2.0:** Introduced computer vision for automatic generation of `alt` attributes for images.\n* **Baton 4.0.\\*:** Unleashed a suite of powerful AI functionalities!\n  * Automatic translations (integrates with `django-modeltranslation`).\n  * Text summarization for content creation.\n  * Text corrections for improved writing.\n  * Image generation using DALL·E 3.\n  * This version also introduced robust theme support, making customization easier than ever. Most theme changes no longer require recompiling the JavaScript application.\n\n\u003e **🎨 Explore Themes!**\n\u003e Discover ready-to-use themes and get inspiration from the `django-baton-themes` repository:\n\u003e [**github.com/otto-torino/django-baton-themes**](https://github.com/otto-torino/django-baton-themes)\n\n---\n\n![Django Baton AI Features Showcase](docs/images/baton-ai.gif)\n*An example of Baton's AI capabilities in action.*\n\n## 📋 Table of Contents\n\n* [Key Features](#key-features)\n* [Installation](#installation)\n* [Configuration](#configuration)\n  * [AI Configuration](#ai-configuration)\n  * [Menu Configuration](#menu-configuration)\n  * [Search Field Configuration](#search-field-configuration)\n* [Baton AI In-Depth](#baton-ai-in-depth)\n* [Page Detection](#page-detection)\n* [Signals](#signals)\n* [JS Utilities](#js-utilities)\n* [JS Translations](#js-translations)\n* [List Filters](#list-filters)\n* [Changelist Includes](#changelist-includes)\n* [Changelist Filters Includes](#changelist-filters-includes)\n* [Changelist Row Attributes](#changelist-row-attributes)\n* [Form Tabs](#form-tabs)\n* [Form Includes](#form-includes)\n* [Collapsable Stacked Inlines](#collapsable-stacked-inlines)\n* [Themes \u0026 Customization](#themes-customization)\n* [Tests](#tests)\n* [Development](#development)\n* [Contributing](#contributing)\n* [Star History](#star-history)\n\n## ⭐ \u003ca name=\"key-features\"\u003eKey Features\u003c/a\u003e\n\n\u003e **Compatibility Notes:**\n\u003e\n\u003e * For **Django \u003e= 5.x**: Use Baton \u003e= 5.0\n\u003e * For **5.x \u003e Django \u003e= 2.1**: Use Baton == 4.x\n\u003e * For **older Django versions (1.x)**: Use `django-baton==1.13.2`\n\nBaton is designed with a core principle: **minimize overriding Django templates**. Styling is primarily achieved through CSS, with JavaScript used for dynamic functionalities.\n\n* **Modern Stack:** Built with Bootstrap 5 and Google Material Symbols.\n* **Fully Responsive:** Adapts seamlessly to all screen sizes.\n* **🧠 AI Powered:**\n  * Automatic translations (integrates with `django-modeltranslation`).\n  * Text summarization and correction.\n  * Image vision (description generation for `alt` text).\n  * Image generation (e.g., DALL·E 3).\n  * *(Requires a Baton subscription key for AI features).*\n* **Customizable Menu:** Flexible, dict-configurable sidebar navigation.\n* **🎨 Theme Support:** Easily customize the look and feel.\n* **Enhanced Search:** Configurable global search field with autocomplete.\n* **Advanced List Filters:** Includes text input, dropdown, and multiple-choice filter options.\n* **Improved Forms:**\n  * Out-of-the-box tabbed interface for fieldsets and inlines.\n  * Fixed submit row for better usability on long forms.\n  * Collapsable entries for stacked inlines.\n  * Lazy loading for uploaded images and image previews.\n* **Flexible Includes:** Easily inject custom templates into changelist and change form pages.\n* **Dynamic Row Attributes:** Add custom HTML attributes (classes, data-attributes, titles) to changelist rows or cells.\n* **User Experience Enhancements:**\n  * Optional modal display for changelist filters.\n  * Optional \"form mode\" for changelist filters (apply multiple filters at once).\n  * Confirmation for unsaved changes.\n  * Loading indicator for multipart form uploads.\n  * Toast notifications for admin messages.\n  * Gravatar support.\n* **Developer Friendly:** Customization via CSS variables or by recompiling the provided JS application for deeper changes.\n* **Translations:** Includes Italian (IT) and Farsi (FA) translations.\n\n**Frontend Technologies:**\nBaton 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.\n\n## 🛠️ \u003ca name=\"installation\"\u003eInstallation\u003c/a\u003e\n\n1. **Install via pip:**\n\n    ```bash\n    pip install django-baton\n    ```\n\n    Alternatively, to use the latest development version, clone the repository into your project:\n\n    ```bash\n    git clone [https://github.com/otto-torino/django-baton.git](https://github.com/otto-torino/django-baton.git)\n    ```\n\n2. **Add to `INSTALLED_APPS`:**\n    In your project's `settings.py`, add `baton` **before** `django.contrib.admin` and `baton.autodiscover` at the **very end** of the list:\n\n    ```python\n    # settings.py\n    INSTALLED_APPS = [\n        # ... other apps ...\n        'baton',  # Must be before django.contrib.admin\n        'django.contrib.admin',\n        # ... other apps ...\n        'baton.autodiscover', # Must be the last app\n    ]\n    ```\n\n3. **Run Migrations:**\n\n    ```bash\n    python manage.py migrate\n    ```\n\n4. **Update URLs:**\n    Replace `django.contrib.admin` with `baton.autodiscover.admin` in your project's main `urls.py` file and include Baton's URLs:\n\n    ```python\n    # urls.py\n    # from django.contrib import admin # Remove or comment out this line\n    from baton.autodiscover import admin # Import Baton's admin\n    from django.urls import path, include\n\n    urlpatterns = [\n        path('admin/', admin.site.urls),\n        path('baton/', include('baton.urls')),\n        # ... your other url patterns ...\n    ]\n    ```\n\n### Why two entries in `INSTALLED_APPS`?\n\n* `baton`: Needs to be placed *before* `django.contrib.admin` because it overrides some of Django's default admin templates and resets CSS.\n* `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.\n\n## ⚙️ \u003ca name=\"configuration\"\u003eConfiguration\u003c/a\u003e\n\nDefine the `BATON` dictionary in your `settings.py` to customize various aspects of the admin interface.\n\n```python\n# settings.py\nfrom baton.ai import AIModels # If using AI features\n\nBATON = {\n    'SITE_HEADER': 'Baton Administration',\n    'SITE_TITLE': 'Baton Admin',\n    'INDEX_TITLE': 'Site Administration Dashboard',\n    'SUPPORT_HREF': 'https://github.com/otto-torino/django-baton/issues',\n    'COPYRIGHT': 'copyright © 2025 \u003ca href=\"https://www.otto.to.it\"\u003eOtto srl\u003c/a\u003e', # HTML is safe\n    'POWERED_BY': '\u003ca href=\"https://www.otto.to.it\"\u003eOtto srl\u003c/a\u003e', # HTML is safe\n    'CONFIRM_UNSAVED_CHANGES': True,\n    'SHOW_MULTIPART_UPLOADING': True,\n    'ENABLE_IMAGES_PREVIEW': True,\n    'CHANGELIST_FILTERS_IN_MODAL': False,\n    'CHANGELIST_FILTERS_ALWAYS_OPEN': False,\n    'CHANGELIST_FILTERS_FORM': False,\n    'CHANGEFORM_FIXED_SUBMIT_ROW': True,\n    'COLLAPSABLE_USER_AREA': True,\n    'MENU_ALWAYS_COLLAPSED': False,\n    'MENU_TITLE': 'Main Menu',\n    'MESSAGES_TOASTS': False, # True for all, or e.g. ['warning', 'error']\n    'GRAVATAR_DEFAULT_IMG': 'retro',\n    'GRAVATAR_ENABLED': True,\n    'LOGIN_SPLASH': '/static/core/img/login-splash.png', # Path to your login splash image\n    'FORCE_THEME': None, # 'light' or 'dark', or None to allow user toggle\n    'BATON_CLIENT_ID': 'your_client_id_for_ai_features',\n    'BATON_CLIENT_SECRET': 'your_client_secret_for_ai_features',\n    'IMAGE_PREVIEW_WIDTH': 200,\n    'AI': {\n        # \"MODELS\": \"myapp.utils.get_ai_models_config\", # Path to a function\n        \"IMAGES_MODEL\": AIModels.BATON_DALL_E_3,\n        \"VISION_MODEL\": AIModels.BATON_GPT_4O_MINI,\n        \"SUMMARIZATIONS_MODEL\": AIModels.BATON_GPT_4O_MINI,\n        \"TRANSLATIONS_MODEL\": AIModels.BATON_GPT_4O,\n        'ENABLE_TRANSLATIONS': True,\n        'ENABLE_CORRECTIONS': True,\n        'CORRECTION_SELECTORS': [\n            \"textarea\",\n            \"input[type=text]:not(.vDateField):not([name=username]):not([name*=subject_location])\"\n        ],\n        \"CORRECTIONS_MODEL\": AIModels.BATON_GPT_3_5_TURBO,\n    },\n    'MENU': (\n        { 'type': 'title', 'label': 'Main Navigation', 'apps': ('auth', ), 'icon': 'apps'},\n        {\n            'type': 'app',\n            'name': 'auth',\n            'label': 'Authentication',\n            'icon': 'lock',\n            'models': (\n                { 'name': 'user', 'label': 'Users', 'icon': 'group' },\n                { 'name': 'group', 'label': 'Groups', 'icon': 'verified_user' },\n            )\n        },\n        { 'type': 'title', 'label': 'Content Management', 'apps': ('flatpages', ), 'icon': 'web_stories' },\n        { 'type': 'model', 'label': 'Static Pages', 'name': 'flatpage', 'app': 'flatpages', 'icon': 'article' },\n        { 'type': 'free', 'label': 'Custom Link', 'url': 'https://www.google.com', 'icon': 'link', 'perms': ('flatpages.add_flatpage', 'auth.change_user') },\n        {\n            'type': 'free',\n            'label': 'Nested Menu',\n            'icon': 'menu_open',\n            'default_open': True,\n            'children': [\n                { 'type': 'model', 'label': 'A Model', 'name': 'mymodelname', 'app': 'myapp', 'icon': 'settings' },\n                { 'type': 'free', 'label': 'Another Link', 'url': 'https://www.example.com', 'icon': 'public' },\n            ]\n        },\n    )\n}\n```\n\n**Detailed Configuration Options:**\n\n* `SITE_HEADER`, `COPYRIGHT`, `POWERED_BY`: Safe for HTML content.\n* `SUPPORT_HREF`: URL for a support link.\n* `CONFIRM_UNSAVED_CHANGES` (Default: `True`): Prompts if leaving a dirty form. (*Note: Relies on jQuery `serialize()`, may not detect all changes.*)\n* `SHOW_MULTIPART_UPLOADING` (Default: `True`): Shows spinner on multipart form submission.\n* `ENABLE_IMAGES_PREVIEW` (Default: `True`): Displays image previews. Customize with `.baton-image-preview` CSS.\n* `CHANGELIST_FILTERS_IN_MODAL` (Default: `False`): If `True`, filters are in a modal.\n* `CHANGELIST_FILTERS_ALWAYS_OPEN` (Default: `False`): If `True` (and modal filters `False`), filters are open by default.\n* `CHANGELIST_FILTERS_FORM` (Default: `False`): If `True`, treats filters as a form.\n* `CHANGEFORM_FIXED_SUBMIT_ROW` (Default: `True`): Fixes submit row at the bottom.\n* `COLLAPSABLE_USER_AREA`: If `True`, user area in sidebar is initially collapsed. (Check docs for default).\n* `MENU_ALWAYS_COLLAPSED` (Default: `False`): If `True`, menu is collapsed by default.\n* `MENU_TITLE` (Default: `'Menu'`): Sidebar menu title.\n* `MESSAGES_TOASTS` (Default: `False`): Use toasts for admin messages (`True` for all, or list like `['warning', 'error']`).\n* `GRAVATAR_DEFAULT_IMG` (Default: `'retro'`): Fallback Gravatar image.\n* `GRAVATAR_ENABLED` (Default: `True`): Show user Gravatar.\n* `LOGIN_SPLASH`: Path to login page background image.\n* `FORCE_THEME` (Default: `None`): `'light'` or `'dark'` to force theme.\n* `BATON_CLIENT_ID`, `BATON_CLIENT_SECRET`: Subscription keys for AI features from [baton.sqrt64.it](https://baton.sqrt64.it).\n* `IMAGE_PREVIEW_WIDTH` (Default: `200`): Width (px) for `BatonAiImageField` preview.\n\n### \u003ca name=\"ai-configuration\"\u003eAI Configuration\u003c/a\u003e\n\nDjango Baton integrates AI to assist with content creation and management.\n\n**Available Models (in `baton.ai.AIModels`):**\n\n* `BATON_GPT_3_5_TURBO`, `BATON_GPT_4_TURBO`, `BATON_GPT_4O`: For translations, summarizations, corrections.\n* `BATON_GPT_4O_MINI`: Default for non-image text tasks and image vision.\n* `BATON_DALL_E_3`: Default for image generation.\n\n**Configuration:**\nSet preferred models in `BATON['AI']`:\n\n```python\n\"AI\": {\n    \"IMAGES_MODEL\": AIModels.BATON_DALL_E_3,\n    \"VISION_MODEL\": AIModels.BATON_GPT_4O_MINI,\n    # ... etc.\n}\n```\n\nOr use a function path via `\"MODELS\": \"myapp.utils.get_ai_models_config\"`.\n\n**Translations:**\nRequires `django-modeltranslation`. Enable and set model:\n\n```python\n'BATON_CLIENT_ID': 'your_client_id',\n'BATON_CLIENT_SECRET': 'your_client_secret',\n'AI': {\n    'ENABLE_TRANSLATIONS': True,\n    'TRANSLATIONS_MODEL': AIModels.BATON_GPT_4O,\n    # ...\n},\n```\n\n\u003e **Note:** Review AI translations. Long text translations may require increased server timeouts.\n\n**Corrections:**\n\n```python\n'AI': {\n    'ENABLE_CORRECTIONS': True,\n    'CORRECTIONS_MODEL': AIModels.BATON_GPT_4O,\n    'CORRECTION_SELECTORS': [\n        \"textarea\",\n        \"input[type=text]:not(.vDateField):not([name=username]):not([name*=subject_location])\"\n    ],\n    # ...\n},\n```\n\nAn icon appears near fields matching selectors for corrections. Ctrl + Left Click also triggers.\n![AI Corrections Screenshot](docs/images/ai-corrections.png)\n\n**Summarizations, Image Vision \u0026 Generation:**\nDetailed in the [Baton AI In-Depth](#baton-ai-in-depth) section.\n\n### \u003ca name=\"menu-configuration\"\u003eMenu Configuration\u003c/a\u003e\n\nCustomize the sidebar via `BATON['MENU']`.\n\n**Item Types:**\n\n* `title`: Section header.\n  * `label`, `apps` (optional), `perms` (optional), `children` (optional), `default_open` (optional), `icon` (optional Material Symbol).\n* `app`: Links to a Django app.\n  * `name` (lowercase app label), `label` (optional), `icon` (optional), `models` (optional tuple to customize model list), `default_open` (optional).\n* `model`: Links to a model's changelist.\n  * `name` (lowercase model name), `app` (lowercase app label), `label` (optional), `icon` (optional).\n* `free`: Custom link.\n  * `label`, `url`, `icon` (optional), `perms` (optional), `re` (optional regex for active highlighting), `children` (optional), `default_open` (optional).\n\n\u003e Children of items that themselves have children are ignored.\n\n### \u003ca name=\"search-field-configuration\"\u003eSearch Field Configuration\u003c/a\u003e\n\nAdd an autocomplete search field to the sidebar .\n\n![Search Field Screenshot](docs/images/search-field.png)\n\n```python\n'SEARCH_FIELD': {\n    'label': 'Search contents...', # Placeholder\n    'url': '/api/admin_search/',   # Your search API endpoint\n}\n```\n\nYour API at `url` receives a `text` GET parameter and should return JSON:\n\n```json\n{\n    \"length\": 1,\n    \"data\": [\n        { \"label\": \"Search Result Label\", \"url\": \"/admin/path/to/item/\", \"icon\": \"search\" }\n    ]\n}\n```\n\nExample Django view for the search API:\n\n```python\n# views.py\nfrom django.http import JsonResponse\nfrom django.contrib.admin.views.decorators import staff_member_required\n# from myapp.models import YourModel # Your model\n\n@staff_member_required\ndef admin_search_api(request):\n    text = request.GET.get('text', None)\n    response_data = []\n    # Implement your search logic here\n    # Example:\n    # if text:\n    #     items = YourModel.objects.filter(title__icontains=text)[:10]\n    #     for item in items:\n    #         response_data.append({\n    #             'label': str(item),\n    #             'url': f'/admin/myapp/yourmodel/{item.id}/change/', # Adjust URL\n    #             'icon': 'article', # Material Symbol name\n    #         })\n    return JsonResponse({'length': len(response_data), 'data': response_data})\n```\n\n## 🤖 \u003ca name=\"baton-ai-in-depth\"\u003eBaton AI In-Depth\u003c/a\u003e\n\nAI features require `BATON_CLIENT_ID` and `BATON_CLIENT_SECRET`. See [AI Configuration](#ai-configuration) for model selection.\n\n### Automatic Translations\n\nIf `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.\n\n### Corrections\n\nIf `ENABLE_CORRECTIONS` is `True`, an icon near text fields (matching `CORRECTION_SELECTORS`) and CKEditor fields triggers AI correction. Differences are shown in a modal.\n\n### Text Summarization\n\nDefine `baton_summarize_fields` in your `ModelAdmin`:\n\n```python\n# admin.py\nclass MyModelAdmin(admin.ModelAdmin):\n    # ...\n    baton_summarize_fields = {\n        \"source_field_name_it\": [{ # e.g., 'body_it'\n            \"target\": \"target_field_name_it\", # e.g., 'summary_it'\n            \"words\": 140, # Approximate\n            \"useBulletedList\": True,\n            \"language\": \"it\", # Optional, defaults to Django's current language\n        },\n        # ... more targets for the same source field ...\n        ],\n    }\n```\n\nButtons 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).\n\n### Image Generation\n\nUse `BatonAiImageField` in your model:\n\n```python\n# models.py\nfrom baton.fields import BatonAiImageField\nfrom django.db import models\nfrom django.utils.translation import gettext_lazy as _\n\nclass MyMediaModel(models.Model):\n    ai_generated_image = BatonAiImageField(\n        verbose_name=_(\"AI Generated Image\"),\n        upload_to=\"ai_images/\",\n        subject_location_field='image_subject_location', # Optional: for subject focus\n        alt_field=\"image_alt_text\" # Optional: for AI-generated alt text (see Image Vision)\n    )\n    image_subject_location = models.CharField(max_length=7, default=\"50,50\", blank=True)\n    image_alt_text = models.CharField(max_length=255, blank=True)\n```\n\nA button near the field opens a modal to generate images from text prompts.\nAlternatively, for standard `ImageField`s, add generation capability with JavaScript:\n\n```html\n\u003cscript\u003e\n    Baton.AI.addImageGeneration('{{ widget.name }}'); // widget.name of the ImageField\n\u003c/script\u003e\n```\n\nIntegrates `django-subject-imagefield` features for subject location. Configure preview width via `IMAGE_PREVIEW_WIDTH` in `BATON` settings.\n\n### Image Vision\n\nGenerate `alt` text for images.\n\n1. **Using `BatonAiImageField`**:\n    Set `alt_field`, `alt_chars` (optional), `alt_language` (optional) attributes on the field. Works primarily for images within inlines.\n\n    ```python\n    # models.py\n    image = BatonAiImageField(upload_to=\"news/\", alt_field=\"image_alt_text\", alt_chars=100)\n    image_alt_text = models.CharField(max_length=150, blank=True)\n    ```\n\n2. **Using `ModelAdmin` configuration**:\n    Define `baton_vision_fields` in your `ModelAdmin`:\n\n    ```python\n    # admin.py\n    class MyModelAdmin(admin.ModelAdmin):\n        # ...\n        baton_vision_fields = {\n            #id_form-0-image\": [{ // CSS selector for the image field (can target inlines)\n            \"#id_image_field_name\": [{ // Key must be a CSS selector targeting the image input or its preview\n                \"target\": \"name_of_alt_text_field\", // Name of a CharField in the same model/form\n                \"chars\": 80,                            // Optional: max characters (default 100)\n                \"language\": \"en\",                       // Optional: language for description\n            }],\n        }\n    ```\n\n    A button appears near the specified image field. Clicking it populates the `target` field with the AI-generated description.\n\n### Stats Widget\n\nDisplay 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):\n\n```django\n{% load baton_tags %}\n\n{% baton_ai_stats %}\n```\n\n![Baton AI Stats Widget](docs/images/baton-ai-stats.png)\n\n### AI Hooks\n\nDjango 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 by default. To add support for other WYSIWYG editors or custom input widgets, you need to define JavaScript hooks.\n\nPlace these hook definitions in your `admin/base_site.html` template, **before** the `{% static 'baton/js_snippets/init_baton.js' %}` script tag:\n\n```html\n\u003cscript src=\"{% static 'baton/app/dist/baton.min.js' %}\"\u003e\u003c/script\u003e {# Ensure Baton's main JS is loaded first #}\n\u003cscript\u003e\n(function () {\n    // Hook to get a list of all field IDs managed by your custom editor.\n    // Should return an array of strings (field IDs).\n    Baton.AI.getEditorFieldsHook = function () {\n        // Example for a hypothetical 'MyEditor':\n        // if (window.MyEditor \u0026\u0026 typeof window.MyEditor.getAllInstanceIds === 'function') {\n        //   return window.MyEditor.getAllInstanceIds();\n        // }\n        return []; // Implement for your specific editor\n    };\n\n    // Hook to get the content of a specific editor instance by its field ID.\n    // Should return the string content or null/undefined if fieldId is not an editor field.\n    Baton.AI.getEditorFieldValueHook = function (fieldId) {\n        // Example for 'MyEditor':\n        // if (window.MyEditor \u0026\u0026 typeof window.MyEditor.getInstance === 'function') {\n        //   const editorInstance = window.MyEditor.getInstance(fieldId);\n        //   return editorInstance ? editorInstance.getContent() : null;\n        // }\n        return null; // Implement for your specific editor\n    };\n\n    // Hook to set the content of a specific editor instance.\n    // Should return true if the fieldId corresponds to an editor and value was set, false otherwise.\n    Baton.AI.setEditorFieldValueHook = function (fieldId, value) {\n        // Example for 'MyEditor':\n        // if (window.MyEditor \u0026\u0026 typeof window.MyEditor.getInstance === 'function') {\n        //   const editorInstance = window.MyEditor.getInstance(fieldId);\n        //   if (editorInstance) {\n        //     editorInstance.setContent(value);\n        //     return true;\n        //   }\n        // }\n        return false; // Implement for your specific editor\n    };\n\n    // Hook to display a \"correct\" icon (checkmark) near an editor field.\n    // `iconElement` is a DOM element (the icon) provided by Baton.\n    // Should return true if successful, false otherwise.\n    Baton.AI.setEditorFieldCorrectHook = function (fieldId, iconElement) {\n        // Example for 'MyEditor':\n        // if (window.MyEditor \u0026\u0026 typeof window.MyEditor.getInstance === 'function') {\n        //   const editorInstance = window.MyEditor.getInstance(fieldId);\n        //   if (editorInstance \u0026\u0026 editorInstance.getContainer()) {\n        //     // Insert iconElement after the editor's container\n        //     editorInstance.getContainer().parentNode.insertBefore(iconElement, editorInstance.getContainer().nextSibling);\n        //     return true;\n        //   }\n        // }\n        return false; // Implement for your specific editor\n    };\n})();\n\u003c/script\u003e\n\u003cscript src=\"{% static 'baton/js_snippets/init_baton.js' %}\"\u003e\u003c/script\u003e\n```\n\n## 📄 \u003ca name=\"page-detection\"\u003ePage Detection\u003c/a\u003e\n\nBaton 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`:\n\n```html\n{{ conf|json_script:\"baton-config\" }} {# Assuming conf is your BATON settings dict passed to template #}\n\u003cscript src=\"{% static 'baton/app/dist/baton.min.js' %}\"\u003e\u003c/script\u003e\n\u003cscript\u003e\n(function () {\n    Baton.detectPageHook = function (defaultDetectFn) {\n        if (/newschange/.test(location.pathname)) { // Example: custom URL part\n            return 'change_form';\n        }\n        return defaultDetectFn(); // Fallback to Baton's default detection\n    };\n})();\n\u003c/script\u003e\n\u003cscript src=\"{% static 'baton/js_snippets/init_baton.js' %}\"\u003e\u003c/script\u003e\n```\n\n**Available Page Types:** `dashboard`, `admindocs`, `login`, `logout`, `password_change`, `password_change_success`, `add_form`, `change_form`, `changelist`, `filer`, `default`.\n\n## 📡 \u003ca name=\"signals\"\u003eSignals\u003c/a\u003e\n\nBaton emits JavaScript events using its dispatcher. Register listeners **before** `Baton.init()`.\n\n```html\n\u003cscript\u003e\n(function ($) { // jQuery is available as $ via Baton\n    Baton.Dispatcher.register('onReady', function () { console.log('BATON IS READY'); });\n    Baton.Dispatcher.register('onMenuReady', function () { console.log('BATON MENU IS READY'); });\n    Baton.Dispatcher.register('onNavbarReady', function () { console.log('BATON NAVBAR IS READY'); });\n    Baton.Dispatcher.register('onTabsReady', function () { console.log('BATON TABS ARE READY'); });\n    Baton.Dispatcher.register('onTabChanged', function (evtName, tabData) { console.log('BATON TAB CHANGED', tabData); });\n    Baton.Dispatcher.register('onMenuError', function () { console.error('BATON MENU FAILED TO LOAD'); });\n})(Baton.jQuery); // Pass Baton's jQuery instance\n\u003c/script\u003e\n\u003cscript src=\"{% static 'baton/js_snippets/init_baton.js' %}\"\u003e\u003c/script\u003e\n```\n\n**Events:**\n\n* `onReady`: Baton JS fully initialized.\n* `onNavbarReady`: Navbar rendered.\n* `onMenuReady`: Menu rendered (often last, due to async fetch).\n* `onTabsReady`: Form tabs rendered.\n* `onTabChanged`: Active form tab changed.\n* `onMenuError`: Menu content failed to load.\n\n## 🧩 \u003ca name=\"js-utilities\"\u003eJS Utilities\u003c/a\u003e\n\nBaton exports JS modules for use in your custom admin scripts.\n\n### Dispatcher\n\nA singleton Mediator pattern implementation.\n\n```javascript\n// Register callback\nBaton.Dispatcher.register('myCustomEvent', function (eventName, eventData) {\n    console.log('Event ' + eventName + ' fired with data: ', eventData);\n});\n\n// Emit event\nBaton.Dispatcher.emit('myCustomEvent', { message: 'Hello Baton!' });\n```\n\n### Modal\n\nCreate Bootstrap modals programmatically.\n![Modal Screenshot](docs/images/modals.png)\n\n```javascript\n// Example Modal Configuration Object:\n// let config = {\n//     title: 'My modal title',\n//     subtitle: 'My subtitle', // optional\n//     content: '\u003cp\u003emy html content\u003c/p\u003e', // alternative to url\n//     url: '/my/url', // fetches content via AJAX; alternative to content.\n//     hideFooter: false, // optional\n//     showBackBtn: false, // optional, show a back button\n//     backBtnCb: function () {}, // optional, back button click callback\n//     actionBtnLabel: 'save', // optional, default 'save'\n//     actionBtnCb: null, // optional, action button callback\n//     onUrlLoaded: function () {}, // optional, callback after AJAX content loads\n//     size: 'lg', // optional: sm, md, lg, xl\n//     onClose: function () {} // optional, callback when modal closes\n// };\n\nlet myModal = new Baton.Modal({\n    title: 'My Modal Title',\n    content: '\u003cp\u003eSome HTML content for the modal body.\u003c/p\u003e',\n    size: 'lg' // Example size\n});\n\nmyModal.open();\n// myModal.close();\n// myModal.toggle();\n// myModal.update({ title: 'New Modal Title', content: '\u003cp\u003eUpdated content here.\u003c/p\u003e' });\n```\n\n## 🌐 \u003ca name=\"js-translations\"\u003eJS Translations\u003c/a\u003e\n\nBaton includes `en` and `it` translations for its JS messages. It detects user locale from `\u003chtml\u003e` tag's `lang` attribute. Add/override translations by defining `Baton.translations` **before** `Baton.init()`:\n\n```javascript\n// Place in admin/base_site.html before init_baton.js\nBaton.translations = {\n  // Default English, override or add other locales\n  en: {\n    unsavedChangesAlert: 'You have some unsaved changes.',\n    uploading: 'Uploading...',\n    filter: 'Filter',\n    close: 'Close',\n    save: 'Save',\n    search: 'Search',\n    cannotCopyToClipboardMessage: 'Cannot copy to clipboard, please do it manually: Ctrl+C, Enter',\n    retrieveDataError: 'There was an error retrieving the data',\n    lightTheme: 'Light theme',\n    darkTheme: 'Dark theme'\n  },\n  it: { // Example for Italian\n    unsavedChangesAlert: 'Ci sono modifiche non salvate.',\n    uploading: 'Caricamento...',\n    // ... other Italian translations\n  }\n  // Add other locales as needed, e.g. 'es': { ... }\n};\n```\n\nBaton defaults to `en` if a translation for the user's locale is not found.\n\n## 📊 \u003ca name=\"list-filters\"\u003eList Filters\u003c/a\u003e\n\n![List Filters Screenshot](docs/images/filters.png)\n\n### Input Text Filters\n\nCreate text input filters in your `ModelAdmin`. (Adapted from [this article](https://medium.com/@hakibenita/how-to-add-a-text-filter-to-django-admin-5d1db93772d8)).\n\n```python\n# admin.py\nfrom baton.admin import InputFilter\nfrom django.contrib import admin # If not already imported\n\nclass MyModelIdFilter(InputFilter):\n    parameter_name = 'id' # URL query parameter\n    title = 'ID'          # Display title for the filter\n\n    def queryset(self, request, queryset):\n        if self.value() is not None:\n            # Ensure value is treated as expected type, e.g., int for ID\n            try:\n                search_term = int(self.value())\n                return queryset.filter(id=search_term)\n            except ValueError:\n                return queryset.none() # Or handle error appropriately\n        return queryset\n\nclass MyModelAdmin(admin.ModelAdmin):\n    list_display = ('id', 'name', 'other_field') # Example\n    list_filter = (MyModelIdFilter, 'other_field')\n```\n\n### Dropdown Filters\n\nProvides dropdown versions of standard Django admin list filters if a filter has at least 3 options. (Inspired by `django-admin-list-filter-dropdown`).\n\n| Django Admin Filter      | Baton Equivalent            |\n| :----------------------- | :-------------------------- |\n| `SimpleListFilter`       | `SimpleDropdownFilter`      |\n| `AllValuesFieldListFilter` | `DropdownFilter`            |\n| `ChoicesFieldListFilter` | `ChoicesDropdownFilter`     |\n| `RelatedFieldListFilter` | `RelatedDropdownFilter`     |\n| `RelatedOnlyFieldListFilter`| `RelatedOnlyDropdownFilter` |\n\nUsage:\n\n```python\n# admin.py\nfrom baton.admin import DropdownFilter, RelatedDropdownFilter, ChoicesDropdownFilter\n# from myapp.models import MyModel, MyRelatedModel # Your models\n\nclass MyModelAdmin(admin.ModelAdmin):\n    # list_display = ('name', 'char_field', 'choice_field', 'foreign_key_field') # Example\n    list_filter = (\n        ('char_field', DropdownFilter), # For CharField, TextField etc.\n        ('choice_field', ChoicesDropdownFilter), # For fields with choices\n        ('foreign_key_field', RelatedDropdownFilter), # For ForeignKey, ManyToManyField\n    )\n```\n\n### Multiple Choice Filters\n\nFilter on multiple options for a field.\n\n```python\n# admin.py\nfrom baton.admin import MultipleChoiceListFilter\n# from myapp.models import News # Assuming News model with Status choices\n\nclass NewsStatusListFilter(MultipleChoiceListFilter):\n    title = 'Status'\n    parameter_name = 'status__in' # Query parameter for __in lookup\n\n    def lookups(self, request, model_admin):\n        # Example assuming News.Status has .choices attribute\n        # return News.Status.choices\n        return (('draft', 'Draft'), ('published', 'Published'), ('archived', 'Archived')) # Example choices\n\nclass NewsAdmin(admin.ModelAdmin):\n    # list_display = ('title', 'status') # Example\n    list_filter = (NewsStatusListFilter, 'publication_date')\n```\n\n## ➕ \u003ca name=\"changelist-includes\"\u003eChangelist Includes\u003c/a\u003e\n\u003e\n\u003e Requires browser support for HTML `\u003ctemplate\u003e` tags.\n\nEmbed custom templates within the changelist page.\n\n```python\n# admin.py\n# from django.contrib import admin # If using @admin.register\n# from myapp.models import News # Your model\n\n# @admin.register(News)\nclass NewsAdmin(admin.ModelAdmin):\n    # ...\n    baton_cl_includes = [\n        ('myapp/admin_includes/cl_top_banner.html', 'top'),\n        ('myapp/admin_includes/cl_below_table.html', 'below'),\n    ]\n```\n\n![Changelist Includes Screenshot](docs/images/baton-cl-includes.png)\n\n**Positions:**\n\n| Position | Description                                     |\n| :------- | :---------------------------------------------- |\n| `top`    | Inside changelist form, at the top.             |\n| `bottom` | Inside changelist form, at the bottom.          |\n| `above`  | Above the entire changelist form.               |\n| `below`  | Below the entire changelist form.               |\n\nChangelist view context variables are available in your included template.\n\n## ☰ \u003ca name=\"changelist-filters-includes\"\u003eChangelist Filters Includes\u003c/a\u003e\n\u003e\n\u003e Requires browser support for HTML `\u003ctemplate\u003e` tags.\n\nEmbed custom templates within the changelist filter container.\n\n```python\n# admin.py\nclass NewsAdmin(admin.ModelAdmin):\n    # ...\n    baton_cl_filters_includes = [\n        ('myapp/admin_includes/filters_top_custom_filter.html', 'top'),\n        ('myapp/admin_includes/filters_bottom_info.html', 'bottom'),\n    ]\n```\n\n![Changelist Filters Includes Screenshot](docs/images/baton-cl-filters-includes.png)\n\n**Positions:**\n\n| Position | Description                                          |\n| :------- | :--------------------------------------------------- |\n| `top`    | Inside filter container, at the top.                 |\n| `bottom` | Inside filter container, at the bottom.              |\n\nChangelist view context variables are available.\n\n## ↔️ \u003ca name=\"changelist-row-attributes\"\u003eChangelist Row Attributes\u003c/a\u003e\n\u003e\n\u003e Requires browser support for HTML `\u003ctemplate\u003e` tags.\n\nAdd HTML attributes (classes, `data-*`, `title`, etc.) to elements in the changelist table (rows, cells).\n![Changelist Row Attributes Screenshot](docs/images/baton-cl-row-attributes.png)\n\n1. Define `baton_cl_rows_attributes` method in your `ModelAdmin`. It takes `request` and `cl` (changelist instance) as arguments.\n2. Return a JSON string dictionary. Keys usually match instance IDs. Values are dicts specifying attributes and selectors.\n\n```python\n# admin.py\nimport json\nfrom django.utils.safestring import mark_safe\n# from myapp.models import News # Assuming News model\n\nclass NewsAdmin(admin.ModelAdmin):\n    list_display = ('title', 'get_category_display', 'status') # Use the method name\n\n    def get_category_display(self, instance):\n        # Helper for targeting specific cells if needed by selector\n        if instance.category: # Check if category exists\n            return mark_safe(f'\u003cspan class=\"category-span-{instance.category.id}\"\u003e{instance.category.name}\u003c/span\u003e')\n        return \"-\" # Fallback if no category\n    get_category_display.short_description = 'Category'\n    get_category_display.admin_order_field = 'category' # Optional: if you want to allow ordering\n\n    def baton_cl_rows_attributes(self, request, cl):\n        data = {}\n        # Example 1: Add 'table-info' class to rows of news items in category ID 2\n        for news_item in cl.queryset.filter(category__id=2):\n            data[str(news_item.id)] = { # Ensure key is string for JSON\n                'class': 'table-info',\n            }\n\n        # Example 2: More complex - target a specific cell for a specific news item\n        # This example assumes you want to style a cell for a news item with ID=1 and category_id=1\n        try:\n            news_to_style = cl.queryset.get(id=1, category__id=1) # More specific lookup\n            data[f\"customkey_cell_{news_to_style.id}\"] = { # Key can be arbitrary if selector is specific\n                'class': 'table-success font-weight-bold', # Example: bold success\n                'data-category-name': news_to_style.category.name if news_to_style.category else '',\n                'title': f'Special: {news_to_style.title}',\n                # This selector targets the span created by get_category_display\n                # It assumes the changelist renders the output of get_category_display in a cell\n                'selector': f'#result_list tr input[name=_selected_action][value=\"{news_to_style.pk}\"] ~ td .category-span-{news_to_style.category_id}',\n                'getParent': 'td', # Applies attributes to the parent \u003ctd\u003e of the found span\n            }\n        except cl.model.DoesNotExist: # Or your specific model DoesNotExist\n            pass # Item not found, or doesn't match criteria\n\n        return json.dumps(data)\n```\n\n**Rules for the returned dictionary values:**\n\n* **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.\n* **`selector`** (optional): CSS selector to find the target element.\n  * 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.\n* **`getParent`** (optional):\n  * Default: `'tr'` (attributes are applied to the row).\n  * You can specify another selector (e.g., `'td'`, `'.field-my_field'`) to find a parent of the element matched by `selector`.\n  * Set to `false` (boolean, not string) or an empty string to apply attributes directly to the element matched by `selector`.\n* **Other keys:** Treated as HTML attributes to be added to the target element.\n\n## 📑 \u003ca name=\"form-tabs\"\u003eForm Tabs\u003c/a\u003e\n\n![Form Tabs Screenshot](docs/images/tabs.png)\nOrganize your admin forms with tabs for fieldsets and inlines. Titles are derived automatically.\n\n**Configuration (in `ModelAdmin.fieldsets` or `ModelAdmin.inlines`):**\n\n```python\n# admin.py\n# from myapp.models import Attribute, Feature # Your models\n\n# class AttributeInline(admin.StackedInline):\n#     model = Attribute # Your model\n#     extra = 1\n\n# class FeatureInline(admin.StackedInline):\n#     model = Feature # Your model\n#     extra = 1\n\nclass ItemAdmin(admin.ModelAdmin):\n    # list_display = ('label', 'description', 'main_feature')\n    # inlines = [AttributeInline, FeatureInline] # Order of inlines matters for grouping\n\n    fieldsets = (\n        ('Main Info', { # This fieldset will be the first tab (or part of it)\n            'fields': ('label', 'description'),\n            'classes': ('baton-tabs-init', 'order-0', 'baton-tab-group-main--inline-attribute'),\n            # 'baton-tabs-init': REQUIRED on the first fieldset to enable tabs.\n            # 'order-X': (Optional) Defines the tab order for this fieldset. Default 0.\n            # 'baton-tab-inline-MODELNAME': Creates a tab for the inline 'attribute' (lowercase model name).\n            # 'baton-tab-fs-CUSTOMNAME': Creates a tab for this fieldset (Main Info -\u003e content_tab).\n            # 'baton-tab-group-GROUPNAME--item1type-ITEMNAME--item2type-ITEMNAME': Creates a group tab.\n            #    GROUPNAME is arbitrary. ITEMNAME can be fs-FIELDSETNAME or inline-INLINEMODELNAME.\n            #    Example: 'baton-tab-group-overview--fs-main_info--inline-attribute'\n            #    This creates a group tab named \"Overview\" containing the \"Main Info\" fieldset and the \"Attribute\" inline.\n            'description': 'This is the main information for the item.'\n        }),\n        ('Content Details', {\n            'fields': ('text', ),\n            'classes': ('baton-tab-fs-content', ), # This fieldset becomes a tab named \"Content\"\n            'description': 'Detailed content for the item.'\n        }),\n        ('Technical Specs', {\n            'fields': ('main_feature', ),\n            # This fieldset is part of a group tab defined in the \"Main Info\" fieldset:\n            # e.g. 'baton-tab-group-main--inline-attribute--fs-tech--inline-feature' in 'Main Info' would group this.\n            'classes': ('baton-tab-fs-tech', ),\n            'description': 'Technical specifications and features.'\n        }),\n    )\n```\n\n**Rules for Tab Classes (applied to a fieldset's `classes` tuple):**\n\n* **`baton-tabs-init`**: **Required** on the *first* fieldset definition to activate the tabbing system.\n* **`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`).\n* **`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`.\n* **`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.\n* **`baton-tab-group-GROUPNAME--item1type-ITEM1NAME--item2type-ITEM2NAME...`**: Creates a group tab.\n  * `GROUPNAME` is an arbitrary name for your tab.\n  * `itemXtype` is either `fs` (for fieldset) or `inline`.\n  * `ITEMXNAME` is your `CUSTOMNAME` (for fieldsets) or `MODELNAME`/`RELATEDNAME` (for inlines).\n  * Example: `baton-tab-group-overview--fs-main_content--inline-attributes`\n* Fieldsets without a `baton-tab-fs-*` class that are *not* part of a group will be appended to the first tab.\n* To make a fieldset *always visible* (not part of any tab), add the class `tab-fs-none` to that fieldset.\n\n**Other Tab Features:**\n\n* If a form field has an error, the first tab containing that field is automatically opened.\n* 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.\n\n## 📎 \u003ca name=\"form-includes\"\u003eForm Includes\u003c/a\u003e\n\u003e\n\u003e Requires browser support for HTML `\u003ctemplate\u003e` tags.\n\nEmbed custom templates near specific fields in change forms.\n\n```python\n# admin.py\nclass NewsAdmin(admin.ModelAdmin):\n    # ...\n    baton_form_includes = [\n        ('myapp/admin_includes/datetime_helper.html', 'publication_date', 'top'),\n        ('myapp/admin_includes/content_notes.html', 'body_content', 'above'),\n        ('myapp/admin_includes/field_icon.html', 'title', 'right'),\n    ]\n```\n\n![Form Includes Screenshot](docs/images/baton-form-includes.png)\n\n**Positions:**\n\n| Position | Description                                  |\n| :------- | :------------------------------------------- |\n| `top`    | Inside the field's form row, at the top.     |\n| `bottom` | Inside the field's form row, at the bottom.  |\n| `above`  | Above the field's form row.                  |\n| `below`  | Below the field's form row.                  |\n| `right`  | Inline, to the right of the input field.     |\n\nThe `{{ original }}` object (the model instance) is available in your included template. Works with tabs.\n\n**Object Tools Includes:**\nInject templates into the object tools bar (top right of change form). Templates are injected inside a `\u003cul\u003e`.\n\n```python\n# admin.py\nclass NewsAdmin(admin.ModelAdmin):\n    # ...\n    baton_form_object_tools_include = ('myapp/admin_includes/custom_object_action.html', 'left') # or 'right'\n```\n\n![Form Object Tools Includes Screenshot](docs/images/baton_form_object_tools_include.png)\n\n## 🤏 \u003ca name=\"collapsable-stacked-inlines\"\u003eCollapsable Stacked Inlines\u003c/a\u003e\n\n![Collapsable Stacked Inlines Screenshot](docs/images/collapsable_stackedinline.png)\nMake individual entries in `admin.StackedInline` collapsable.\n\nAdd `collapse-entry` to the inline's `classes`:\n\n```python\n# admin.py\nclass VideoInline(admin.StackedInline):\n    # model = Video # Your model\n    extra = 1\n    classes = ('collapse-entry', ) # Can be combined with Django's 'collapse'\n```\n\nTo have the first entry expanded by default:\n\n```python\n# admin.py\nclass VideoInline(admin.StackedInline):\n    # model = Video # Your model\n    extra = 1\n    classes = ('collapse-entry', 'expand-first')\n```\n\n## 🎨 \u003ca name=\"themes-customization\"\u003eThemes \u0026 Customization\u003c/a\u003e\n\nEasily customize Baton's appearance:\n\n1. **CSS Variables:**\n    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).\n    Example:\n\n    ```css\n    /* myapp/static/baton/css/root.css */\n    :root {\n      --bs-primary: #FF6347;\n      --bs-primary-rgb: 255,99,71;\n      --baton-sidebar-active-bg: #FF6347;\n    }\n    ```\n\n2. **Admin Themes:**\n    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.\n    \u003e **Caution:** Theme content is marked safe and injected as-is. Be careful.\n    \u003e ✨ Find ready-to-use themes at [django-baton-themes](https://github.com/otto-torino/django-baton-themes).\n\n3. **Heavy Customization (Recompiling JS App):**\n    For changes to primary/secondary Bootstrap colors or extensive modifications, you can recompile Baton's JavaScript application.\n    ![Customization Screenshot](docs/images/customization.png)\n    1. Clone `django-baton`.\n    2. Navigate to `django-baton/baton/static/baton/app/`.\n    3. Run `npm install`.\n    4. Edit `src/styles/_variables.scss` (and other SCSS/JS files as needed).\n    5. Run `npm run compile`.\n    6. Copy the compiled `dist/baton.min.js` to your project: `YOUR_APP/static/baton/app/dist/`.\n    7. Ensure `YOUR_APP` is listed *before* `baton` in `INSTALLED_APPS`.\n\n    For live development with automatic recompilation:\n    1. `cd django-baton/baton/static/baton/app/`\n    2. Run `npm run dev:baton` (starts Webpack dev server, usually on `http://localhost:8080`).\n    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:\n\n        ```html\n        {# \u003cscript src=\"{% static 'baton/app/dist/baton.min.js' %}\"\u003e\u003c/script\u003e #}\n        \u003cscript src=\"http://localhost:8080/static/baton/app/dist/baton.min.js\"\u003e\u003c/script\u003e\n        ```\n\n    Now, changes in the JS app will auto-update, just refresh your Django admin page.\n\n## 🧪 \u003ca name=\"tests\"\u003eTests\u003c/a\u003e\n\nBaton 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`.\n\n## 💻 \u003ca name=\"development\"\u003eDevelopment\u003c/a\u003e\n\nTo contribute or develop locally:\n\n1. **Set up the test app:**\n\n    ```bash\n    cd testapp\n    python3 -m venv .virtualenv\n    source .virtualenv/bin/activate # On Windows: .virtualenv\\Scripts\\activate\n    cd app\n    pip install -r requirements.txt\n    python manage.py migrate\n    python manage.py createsuperuser # If needed\n    python manage.py runserver\n    ```\n\n    (Default login after `createsuperuser`: `admin` / `admin`, or as you defined).\n\n2. **Enable live JS recompilation for development:**\n    * In `testapp/app/templates/admin/base_site.html`, switch the script source to Webpack dev server:\n\n        ```html\n        {# \u003cscript src=\"{% static 'baton/app/dist/baton.min.js' %}\"\u003e\u003c/script\u003e #}\n        \u003cscript src=\"http://localhost:8080/static/baton/app/dist/baton.min.js\"\u003e\u003c/script\u003e\n        ```\n\n    * In a new terminal, navigate to Baton's frontend app directory and start the dev server:\n\n        ```bash\n        cd /path/to/your/django-baton/baton/static/baton/app/\n        npm install\n        npm run dev # For continuous development, watches for changes\n        ```\n\n    Changes to Baton's frontend app will now auto-recompile. Refresh your browser to see them.\n\n### Commands\n\nInstall `invoke` and `sphinx_rtd_theme` in your Python environment for documentation generation:\n\n```bash\npip install invoke sphinx_rtd_theme\n```\n\nTo generate documentation locally (from the root directory of the `django-baton` repository):\n\n```bash\ninvoke docs\n```\n\n## 🤝 \u003ca name=\"contributing\"\u003eContributing\u003c/a\u003e\n\nPlease read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. We welcome contributions!\n\n## 🌟 \u003ca name=\"star-history\"\u003eStar History\u003c/a\u003e\n\n[![Star History Chart](https://api.star-history.com/svg?repos=otto-torino/django-baton\u0026type=Date)](https://star-history.com/#otto-torino/django-baton\u0026Date)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fotto-torino%2Fdjango-baton","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fotto-torino%2Fdjango-baton","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fotto-torino%2Fdjango-baton/lists"}