├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── django_auto_admin_fieldsets ├── __init__.py └── admin.py ├── pyproject.toml ├── tests ├── manage.py └── testapp │ ├── __init__.py │ ├── settings.py │ ├── test_auto_admin_fieldsets.py │ └── urls.py └── tox.ini /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | tests: 11 | name: Python ${{ matrix.python-version }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: 17 | - "3.10" 18 | - "3.11" 19 | - "3.12" 20 | - "3.13" 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip wheel setuptools tox 31 | - name: Run tox targets for ${{ matrix.python-version }} 32 | run: | 33 | ENV_PREFIX=$(tr -C -d "0-9" <<< "${{ matrix.python-version }}") 34 | TOXENV=$(tox --listenvs | grep "^py$ENV_PREFIX" | tr '\n' ',') python -m tox 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py? 2 | *.sw? 3 | *~ 4 | .coverage 5 | .tox 6 | /*.egg-info 7 | /MANIFEST 8 | build 9 | dist 10 | htmlcov 11 | node_modules 12 | yarn.lock 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: ".yarn/|yarn.lock|\\.min\\.(css|js)$" 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v5.0.0 5 | hooks: 6 | - id: check-added-large-files 7 | - id: check-builtin-literals 8 | - id: check-executables-have-shebangs 9 | - id: check-merge-conflict 10 | - id: check-toml 11 | - id: check-yaml 12 | - id: detect-private-key 13 | - id: end-of-file-fixer 14 | - id: mixed-line-ending 15 | - id: trailing-whitespace 16 | - repo: https://github.com/adamchainz/django-upgrade 17 | rev: 1.22.2 18 | hooks: 19 | - id: django-upgrade 20 | args: [--target-version, "3.2"] 21 | - repo: https://github.com/astral-sh/ruff-pre-commit 22 | rev: "v0.9.3" 23 | hooks: 24 | - id: ruff 25 | args: [--unsafe-fixes] 26 | - id: ruff-format 27 | - repo: https://github.com/biomejs/pre-commit 28 | rev: "v0.6.1" 29 | hooks: 30 | - id: biome-check 31 | additional_dependencies: ["@biomejs/biome@1.9.4"] 32 | args: [--unsafe] 33 | - repo: https://github.com/tox-dev/pyproject-fmt 34 | rev: v2.5.0 35 | hooks: 36 | - id: pyproject-fmt 37 | - repo: https://github.com/abravalheri/validate-pyproject 38 | rev: v0.23 39 | hooks: 40 | - id: validate-pyproject 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.2] - 2024-04-14 11 | 12 | ### Changed 13 | - Modified the `auto_add_fields_to_fieldsets` function to no longer exclude readonly fields 14 | 15 | ## [0.1] - 2024-04-14 16 | 17 | ### Added 18 | - Initial release of django-auto-admin-fieldsets 19 | - Added `AutoFieldsetsMixin` for automatically handling unspecified fields in admin fieldsets 20 | - Added `AutoFieldsetsModelAdmin` as a convenience class 21 | - Added `auto_add_fields_to_fieldsets` utility function 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Matthias Kestenholz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Auto Admin Fieldsets 2 | 3 | A Django utility for automatically handling unspecified fields in ModelAdmin fieldsets. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | pip install django-auto-admin-fieldsets 9 | ``` 10 | 11 | ## Features 12 | 13 | - Automatically add unspecified model fields to a designated placeholder in Django admin fieldsets 14 | - Works with all field types, including many-to-many fields 15 | - Respects `exclude` and `readonly_fields` settings 16 | - Supports custom placeholders 17 | - Provides both a mixin and a standalone function for maximum flexibility 18 | 19 | ## Usage 20 | 21 | ### Using the Mixin 22 | 23 | ```python 24 | from django.contrib import admin 25 | from django_auto_admin_fieldsets.admin import AutoFieldsetsMixin 26 | from . import models 27 | 28 | @admin.register(models.MyModel) 29 | class MyModelAdmin(AutoFieldsetsMixin, admin.ModelAdmin): 30 | # Define fieldsets as usual with a placeholder 31 | fieldsets = [ 32 | ("Basic Information", {"fields": ["title", "slug"]}), 33 | ("Content", {"fields": ["__remaining__"]}), # All other fields will appear here 34 | ] 35 | 36 | # Optional: customize the placeholder (default is "__remaining__") 37 | remaining_fields_placeholder = "__remaining__" 38 | ``` 39 | 40 | ### Using the Convenience ModelAdmin 41 | 42 | ```python 43 | from django.contrib import admin 44 | from django_auto_admin_fieldsets.admin import AutoFieldsetsModelAdmin 45 | from . import models 46 | 47 | @admin.register(models.MyModel) 48 | class MyModelAdmin(AutoFieldsetsModelAdmin): 49 | # Define fieldsets as usual with a placeholder 50 | fieldsets = [ 51 | ("Basic Information", {"fields": ["title", "slug"]}), 52 | ("Content", {"fields": ["__remaining__"]}), # All other fields will appear here 53 | ] 54 | ``` 55 | 56 | ### Using the Standalone Function 57 | 58 | ```python 59 | from django.contrib import admin 60 | from django_auto_admin_fieldsets.admin import auto_add_fields_to_fieldsets 61 | from . import models 62 | 63 | @admin.register(models.MyModel) 64 | class MyModelAdmin(admin.ModelAdmin): 65 | fieldsets = [ 66 | ("Basic Information", {"fields": ["title", "slug"]}), 67 | ("Content", {"fields": ["__remaining__"]}), 68 | ] 69 | 70 | def get_fieldsets(self, request, obj=None): 71 | fieldsets = super().get_fieldsets(request, obj) 72 | return auto_add_fields_to_fieldsets( 73 | model=self.model, 74 | fieldsets=fieldsets, 75 | exclude=self.exclude or [], 76 | placeholder="__remaining__", 77 | ) 78 | 79 | admin.site.register(MyModel, MyModelAdmin) 80 | ``` 81 | 82 | ## Configuration Options 83 | 84 | - `remaining_fields_placeholder`: The placeholder string to look for in your fieldsets (default: `"__remaining__"`) 85 | 86 | The function also respects the standard Django admin configuration options. If 87 | you do not want a field to appear in the resulting `fieldsets`, add it to the 88 | standard `exclude` list. 89 | 90 | ## Development 91 | 92 | ### Running Tests 93 | 94 | Tests should be run using tox, which tests against multiple Python and Django versions: 95 | 96 | ```bash 97 | tox 98 | ``` 99 | 100 | This will run the test suite against all supported Python and Django combinations as defined in tox.ini. 101 | 102 | ### Code Quality 103 | 104 | This project uses pre-commit for code quality checks. After cloning the repository, install the pre-commit hooks: 105 | 106 | ```bash 107 | pre-commit install 108 | ``` 109 | 110 | The pre-commit configuration includes ruff for linting and formatting. Configuration can be found in `pyproject.toml`. 111 | 112 | ## Compatibility 113 | 114 | - Python: 3.10 and above 115 | - Django: 4.2, 5.1, and 5.2 (also tested with Django main) 116 | 117 | ## License 118 | 119 | MIT 120 | -------------------------------------------------------------------------------- /django_auto_admin_fieldsets/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.2.0" 2 | -------------------------------------------------------------------------------- /django_auto_admin_fieldsets/admin.py: -------------------------------------------------------------------------------- 1 | """ 2 | auto_admin_fieldsets - A Django utility for automatically handling unspecified fields in admin fieldsets 3 | 4 | This utility provides a mixin class and a function to automatically add unspecified fields 5 | to a designated placeholder in Django ModelAdmin fieldsets. 6 | """ 7 | 8 | from typing import Any 9 | 10 | from django.contrib import admin 11 | 12 | 13 | class AutoFieldsetsMixin: 14 | """ 15 | Mixin for Django ModelAdmin that automatically adds unspecified fields to a designated placeholder. 16 | 17 | Usage: 18 | Define fieldsets as usual but include a special placeholder value (default is '__remaining__') 19 | where you want remaining fields to appear. 20 | 21 | Example: 22 | fieldsets = [ 23 | ("Basic Information", {"fields": ["title", "slug"]}), 24 | ("Additional", {"fields": ["__remaining__"]}), # All other fields will be added here 25 | ] 26 | """ 27 | 28 | remaining_fields_placeholder = "__remaining__" 29 | 30 | def get_fieldsets(self, request, obj=None): 31 | """ 32 | Override get_fieldsets to automatically add remaining fields to the designated placeholder. 33 | """ 34 | fieldsets = super().get_fieldsets(request, obj) 35 | return auto_add_fields_to_fieldsets( 36 | model=self.model, 37 | fieldsets=fieldsets, 38 | exclude=getattr(self, "exclude", None) or [], 39 | get_fields=lambda: self.get_fields(request, obj), 40 | placeholder=self.remaining_fields_placeholder, 41 | ) 42 | 43 | 44 | class AutoFieldsetsModelAdmin(AutoFieldsetsMixin, admin.ModelAdmin): 45 | """ 46 | ModelAdmin subclass that automatically adds unspecified fields to a designated placeholder. 47 | """ 48 | 49 | 50 | def auto_add_fields_to_fieldsets( 51 | model: Any, 52 | fieldsets: list[tuple[str, dict[str, Any]]], 53 | exclude: list[str] = None, 54 | get_fields=None, 55 | placeholder: str = "__remaining__", 56 | ) -> list[tuple[str, dict[str, Any]]]: 57 | """ 58 | Utility function to automatically add unspecified fields to a designated placeholder in fieldsets. 59 | 60 | Args: 61 | model: The Django model class 62 | fieldsets: The fieldsets list to process 63 | exclude: List of field names to exclude (optional) 64 | get_fields: Function to get all available fields, if needed for custom cases 65 | placeholder: The placeholder string to look for in fieldsets 66 | 67 | Returns: 68 | Updated fieldsets with remaining fields added to the placeholder location 69 | """ 70 | exclude = exclude or [] 71 | 72 | # Get all field names from the model 73 | model_fields = list(model._meta.fields) 74 | # Add many-to-many fields 75 | model_fields.extend(list(model._meta.many_to_many)) 76 | 77 | model_fields = [ 78 | field.name 79 | for field in model_fields 80 | if field.editable and not field.auto_created 81 | ] 82 | 83 | # Get fields that are already specified in fieldsets 84 | specified_fields = set() 85 | placeholder_location = None 86 | 87 | for name, options in fieldsets: 88 | field_list = list(options.get("fields", [])) 89 | for i, field in enumerate(field_list): 90 | if field == placeholder: 91 | placeholder_location = (name, options, i) 92 | elif isinstance(field, list | tuple): 93 | specified_fields.update(field) 94 | else: 95 | specified_fields.add(field) 96 | 97 | # For edge cases where we need the custom get_fields 98 | if get_fields: 99 | available_fields = get_fields() 100 | else: 101 | available_fields = model_fields 102 | 103 | # Find fields that haven't been specified in fieldsets 104 | remaining_fields = [ 105 | f 106 | for f in available_fields 107 | if f not in specified_fields 108 | and f not in exclude 109 | and (not f.startswith("_") or f in available_fields) 110 | ] 111 | 112 | # Add remaining fields to the placeholder location if it exists 113 | if placeholder_location: 114 | name, options, placeholder_index = placeholder_location 115 | field_list = list(options.get("fields", [])) 116 | field_list[placeholder_index : placeholder_index + 1] = remaining_fields 117 | options["fields"] = field_list 118 | 119 | return fieldsets 120 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = [ "hatchling" ] 4 | 5 | [project] 6 | name = "django-auto-admin-fieldsets" 7 | description = "A Django utility for automatically handling unspecified fields in admin fieldsets" 8 | readme = "README.md" 9 | keywords = [ "admin", "django", "fieldsets", "utility" ] 10 | license = { text = "MIT" } 11 | authors = [ 12 | { name = "Matthias Kestenholz", email = "mk@feinheit.ch" }, 13 | ] 14 | requires-python = ">=3.10" 15 | 16 | classifiers = [ 17 | "Development Status :: 4 - Beta", 18 | "Environment :: Web Environment", 19 | "Framework :: Django", 20 | "Framework :: Django :: 4.2", 21 | "Framework :: Django :: 5.0", 22 | "Framework :: Django :: 5.1", 23 | "Framework :: Django :: 5.2", 24 | "Intended Audience :: Developers", 25 | "License :: OSI Approved :: MIT License", 26 | "Operating System :: OS Independent", 27 | "Programming Language :: Python", 28 | "Programming Language :: Python :: 3 :: Only", 29 | "Programming Language :: Python :: 3.10", 30 | "Programming Language :: Python :: 3.11", 31 | "Programming Language :: Python :: 3.12", 32 | "Programming Language :: Python :: 3.13", 33 | "Topic :: Internet :: WWW/HTTP", 34 | "Topic :: Software Development :: Libraries :: Python Modules", 35 | ] 36 | dynamic = [ 37 | "version", 38 | ] 39 | dependencies = [ 40 | ] 41 | optional-dependencies.tests = [ 42 | "coverage", 43 | ] 44 | urls.Documentation = "https://github.com/matthiask/django-auto-admin-fieldsets#readme" 45 | urls.Homepage = "https://github.com/matthiask/django-auto-admin-fieldsets" 46 | urls.Issues = "https://github.com/matthiask/django-auto-admin-fieldsets/issues" 47 | 48 | [tool.hatch.build.targets.wheel] 49 | packages = [ "django_auto_admin_fieldsets" ] 50 | 51 | [tool.hatch.build] 52 | include = [ 53 | "django_auto_admin_fieldsets", 54 | ] 55 | 56 | [tool.hatch.version] 57 | path = "django_auto_admin_fieldsets/__init__.py" 58 | 59 | [tool.ruff] 60 | target-version = "py310" 61 | 62 | fix = true 63 | show-fixes = true 64 | lint.extend-select = [ 65 | # flake8-bugbear 66 | "B", 67 | # flake8-comprehensions 68 | "C4", 69 | # mmcabe 70 | "C90", 71 | # flake8-django 72 | "DJ", 73 | "E", 74 | # pyflakes, pycodestyle 75 | "F", 76 | # flake8-boolean-trap 77 | "FBT", 78 | # flake8-logging-format 79 | "G", 80 | # isort 81 | "I", 82 | # flake8-gettext 83 | "INT", 84 | # pygrep-hooks 85 | "PGH", 86 | # flake8-pie 87 | "PIE", 88 | # pylint 89 | "PLE", 90 | "PLW", 91 | # unused noqa 92 | "RUF100", 93 | # flake8-tidy-imports 94 | "TID", 95 | # pyupgrade 96 | "UP", 97 | "W", 98 | # flake8-2020 99 | "YTT", 100 | ] 101 | lint.extend-ignore = [ 102 | # Allow zip() without strict= 103 | "B905", 104 | # No line length errors 105 | "E501", 106 | ] 107 | lint.per-file-ignores."*/migrat*/*" = [ 108 | # Allow using PascalCase model names in migrations 109 | "N806", 110 | # Ignore the fact that migration files are invalid module names 111 | "N999", 112 | ] 113 | lint.isort.combine-as-imports = true 114 | lint.isort.lines-after-imports = 2 115 | lint.mccabe.max-complexity = 15 116 | -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from os.path import abspath, dirname 5 | 6 | 7 | if __name__ == "__main__": 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") 9 | 10 | sys.path.insert(0, dirname(dirname(abspath(__file__)))) 11 | 12 | from django.core.management import execute_from_command_line 13 | 14 | execute_from_command_line(sys.argv) 15 | -------------------------------------------------------------------------------- /tests/testapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiask/django-auto-admin-fieldsets/ae4b4b48781b8b17bb41e22857abd4298588bbdf/tests/testapp/__init__.py -------------------------------------------------------------------------------- /tests/testapp/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | BASEDIR = os.path.dirname(__file__) 5 | 6 | DATABASES = { 7 | "default": { 8 | "ENGINE": "django.db.backends.sqlite3", 9 | "NAME": os.path.join(BASEDIR, "db.sqlite3"), 10 | } 11 | } 12 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField" 13 | 14 | INSTALLED_APPS = [ 15 | "django.contrib.auth", 16 | "django.contrib.admin", 17 | "django.contrib.contenttypes", 18 | "django.contrib.sessions", 19 | "django.contrib.staticfiles", 20 | "django.contrib.messages", 21 | "testapp", 22 | ] 23 | 24 | MEDIA_ROOT = "/media/" 25 | STATIC_URL = "/static/" 26 | MEDIA_ROOT = os.path.join(BASEDIR, "media/") 27 | STATIC_ROOT = os.path.join(BASEDIR, "static/") 28 | SECRET_KEY = "supersikret" 29 | LOGIN_REDIRECT_URL = "/?login=1" 30 | 31 | ROOT_URLCONF = "testapp.urls" 32 | LANGUAGES = (("en", "English"), ("de", "German"), ("fr", "French")) 33 | 34 | DEBUG = True 35 | TEMPLATES = [ 36 | { 37 | "BACKEND": "django.template.backends.django.DjangoTemplates", 38 | "DIRS": [], 39 | "APP_DIRS": True, 40 | "OPTIONS": { 41 | "context_processors": [ 42 | "django.template.context_processors.debug", 43 | "django.template.context_processors.request", 44 | "django.contrib.auth.context_processors.auth", 45 | "django.contrib.messages.context_processors.messages", 46 | ] 47 | }, 48 | } 49 | ] 50 | 51 | MIDDLEWARE = [ 52 | "django.contrib.sessions.middleware.SessionMiddleware", 53 | "django.middleware.common.CommonMiddleware", 54 | "django.middleware.locale.LocaleMiddleware", 55 | "django.middleware.csrf.CsrfViewMiddleware", 56 | "django.contrib.auth.middleware.AuthenticationMiddleware", 57 | "django.contrib.messages.middleware.MessageMiddleware", 58 | ] 59 | -------------------------------------------------------------------------------- /tests/testapp/test_auto_admin_fieldsets.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.db import models 3 | from django.test import TestCase 4 | 5 | from django_auto_admin_fieldsets.admin import ( 6 | AutoFieldsetsMixin, 7 | auto_add_fields_to_fieldsets, 8 | ) 9 | 10 | 11 | # Test model 12 | class TestModel(models.Model): 13 | title = models.CharField(max_length=100) 14 | slug = models.SlugField() 15 | description = models.TextField() 16 | published = models.BooleanField(default=False) 17 | created_at = models.DateTimeField(auto_now_add=True) 18 | updated_at = models.DateTimeField(auto_now=True) 19 | featured = models.BooleanField(default=False) 20 | 21 | class Meta: 22 | app_label = "tests" # This is just for testing, no migrations needed 23 | 24 | def __str__(self): 25 | return self.title 26 | 27 | 28 | # Test admin classes 29 | class TestAdminWithMixin(AutoFieldsetsMixin, admin.ModelAdmin): 30 | model = TestModel 31 | fieldsets = [ 32 | ("Basic", {"fields": ["title", "slug"]}), 33 | ("Extra", {"fields": ["__remaining__"]}), 34 | ] 35 | 36 | 37 | class TestStandaloneFunction(TestCase): 38 | def test_auto_add_fields_to_fieldsets(self): 39 | # Starting fieldsets with placeholder 40 | fieldsets = [ 41 | ("Basic", {"fields": ["title", "slug"]}), 42 | ("Extra", {"fields": ["__remaining__"]}), 43 | ] 44 | 45 | # Expected result 46 | expected = [ 47 | ("Basic", {"fields": ["title", "slug"]}), 48 | ( 49 | "Extra", 50 | { 51 | "fields": [ 52 | "description", 53 | "published", 54 | "featured", 55 | ] 56 | }, 57 | ), 58 | ] 59 | 60 | # Get the result using the standalone function 61 | result = auto_add_fields_to_fieldsets( 62 | model=TestModel, fieldsets=fieldsets, exclude=[] 63 | ) 64 | 65 | # Check if the result has the expected structure 66 | self.assertEqual(len(result), 2) 67 | self.assertEqual(result[0][0], expected[0][0]) 68 | self.assertEqual(result[0][1]["fields"], expected[0][1]["fields"]) 69 | 70 | # The second fieldset should have all remaining fields 71 | remaining_fields = result[1][1]["fields"] 72 | expected_remaining = expected[1][1]["fields"] 73 | 74 | self.assertEqual(set(remaining_fields), set(expected_remaining)) 75 | 76 | def test_with_field_grouping(self): 77 | # Starting fieldsets with custom placeholder 78 | fieldsets = [ 79 | ("Basic", {"fields": [("title", "slug")]}), 80 | ("Extra", {"fields": ["__remaining__"]}), 81 | ] 82 | 83 | # Get the result using the standalone function 84 | result = auto_add_fields_to_fieldsets( 85 | model=TestModel, fieldsets=fieldsets, exclude=[] 86 | ) 87 | 88 | self.assertEqual( 89 | result, 90 | [ 91 | ("Basic", {"fields": [("title", "slug")]}), 92 | ("Extra", {"fields": ["description", "published", "featured"]}), 93 | ], 94 | ) 95 | 96 | def test_with_custom_placeholder(self): 97 | # Starting fieldsets with custom placeholder 98 | fieldsets = [ 99 | ("Basic", {"fields": ["title", "slug"]}), 100 | ("Extra", {"fields": ["__custom__"]}), 101 | ] 102 | 103 | # Get the result using the standalone function with custom placeholder 104 | result = auto_add_fields_to_fieldsets( 105 | model=TestModel, 106 | fieldsets=fieldsets, 107 | exclude=[], 108 | placeholder="__custom__", 109 | ) 110 | 111 | # The second fieldset should have all remaining fields 112 | remaining_fields = result[1][1]["fields"] 113 | expected_remaining = [ 114 | "description", 115 | "published", 116 | "featured", 117 | ] 118 | 119 | self.assertEqual(set(remaining_fields), set(expected_remaining)) 120 | 121 | def test_with_exclude(self): 122 | # Starting fieldsets with placeholder 123 | fieldsets = [ 124 | ("Basic", {"fields": ["title", "slug"]}), 125 | ("Extra", {"fields": ["__remaining__"]}), 126 | ] 127 | 128 | # Get the result using the standalone function 129 | result = auto_add_fields_to_fieldsets( 130 | model=TestModel, 131 | fieldsets=fieldsets, 132 | exclude=["featured", "published"], # Exclude featured and published fields 133 | ) 134 | 135 | # The second fieldset should have remaining fields minus excluded fields 136 | remaining_fields = result[1][1]["fields"] 137 | expected_remaining = [ 138 | "description", 139 | ] # No featured, published, created_at, updated_at 140 | 141 | self.assertEqual(set(remaining_fields), set(expected_remaining)) 142 | -------------------------------------------------------------------------------- /tests/testapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | 4 | 5 | urlpatterns = [path("admin/", admin.site.urls)] 6 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{310}-dj{42} 4 | py{310,311}-dj{42} 5 | py{312}-dj{42,51,main} 6 | py{313}-dj{51,52,main} 7 | 8 | [testenv] 9 | usedevelop = true 10 | extras = tests 11 | commands = 12 | python -Wd {envbindir}/coverage run tests/manage.py test -v2 --keepdb {posargs:testapp} 13 | coverage report -m 14 | deps = 15 | dj42: Django>=4.2,<5.0 16 | dj51: Django>=5.1,<5.2 17 | dj52: Django>=5.2,<6.0 18 | djmain: https://github.com/django/django/archive/main.tar.gz 19 | 20 | # [testenv:docs] 21 | # deps = 22 | # Sphinx 23 | # sphinx-rtd-theme 24 | # Django 25 | # changedir = docs 26 | # commands = make html 27 | # skip_install = true 28 | # allowlist_externals = make 29 | --------------------------------------------------------------------------------