django-form-matrix 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. django_form_matrix-0.1.0/.github/workflows/compat-matrix.yml +27 -0
  2. django_form_matrix-0.1.0/.github/workflows/js-tests.yml +24 -0
  3. django_form_matrix-0.1.0/.github/workflows/pre-commit.yml +28 -0
  4. django_form_matrix-0.1.0/.github/workflows/pytest.yml +31 -0
  5. django_form_matrix-0.1.0/.github/workflows/release.yml +55 -0
  6. django_form_matrix-0.1.0/.gitignore +42 -0
  7. django_form_matrix-0.1.0/.pre-commit-config.yaml +66 -0
  8. django_form_matrix-0.1.0/CHANGELOG.md +21 -0
  9. django_form_matrix-0.1.0/CONTRIBUTING.md +113 -0
  10. django_form_matrix-0.1.0/LICENSE +21 -0
  11. django_form_matrix-0.1.0/PKG-INFO +217 -0
  12. django_form_matrix-0.1.0/README.md +171 -0
  13. django_form_matrix-0.1.0/biome.json +19 -0
  14. django_form_matrix-0.1.0/docs/examples/permissions.md +189 -0
  15. django_form_matrix-0.1.0/docs/screenshots/.gitkeep +0 -0
  16. django_form_matrix-0.1.0/docs/screenshots/hero.png +0 -0
  17. django_form_matrix-0.1.0/docs/screenshots/permissions.png +0 -0
  18. django_form_matrix-0.1.0/docs/screenshots/unfold-dark.png +0 -0
  19. django_form_matrix-0.1.0/docs/screenshots/unfold-permissions.png +0 -0
  20. django_form_matrix-0.1.0/docs/screenshots/unfold.png +0 -0
  21. django_form_matrix-0.1.0/example/demo/__init__.py +0 -0
  22. django_form_matrix-0.1.0/example/demo/admin.py +36 -0
  23. django_form_matrix-0.1.0/example/demo/dynamic_permissions.py +84 -0
  24. django_form_matrix-0.1.0/example/demo/forms.py +35 -0
  25. django_form_matrix-0.1.0/example/demo/migrations/0001_initial.py +31 -0
  26. django_form_matrix-0.1.0/example/demo/migrations/__init__.py +0 -0
  27. django_form_matrix-0.1.0/example/demo/models.py +22 -0
  28. django_form_matrix-0.1.0/example/demo/templates/demo/custom_matrix.html +7 -0
  29. django_form_matrix-0.1.0/example/demo/templates/demo/custom_permissions.html +7 -0
  30. django_form_matrix-0.1.0/example/example/__init__.py +0 -0
  31. django_form_matrix-0.1.0/example/example/settings.py +66 -0
  32. django_form_matrix-0.1.0/example/example/settings_unfold.py +6 -0
  33. django_form_matrix-0.1.0/example/example/urls.py +6 -0
  34. django_form_matrix-0.1.0/example/example/wsgi.py +7 -0
  35. django_form_matrix-0.1.0/example/manage.py +20 -0
  36. django_form_matrix-0.1.0/package-lock.json +2963 -0
  37. django_form_matrix-0.1.0/package.json +16 -0
  38. django_form_matrix-0.1.0/pyproject.toml +159 -0
  39. django_form_matrix-0.1.0/src/django_form_matrix/__init__.py +6 -0
  40. django_form_matrix-0.1.0/src/django_form_matrix/apps.py +7 -0
  41. django_form_matrix-0.1.0/src/django_form_matrix/contrib/__init__.py +0 -0
  42. django_form_matrix-0.1.0/src/django_form_matrix/contrib/permissions.py +177 -0
  43. django_form_matrix-0.1.0/src/django_form_matrix/contrib/unfold.py +23 -0
  44. django_form_matrix-0.1.0/src/django_form_matrix/fields.py +159 -0
  45. django_form_matrix-0.1.0/src/django_form_matrix/locale/es/LC_MESSAGES/django.mo +0 -0
  46. django_form_matrix-0.1.0/src/django_form_matrix/locale/es/LC_MESSAGES/django.po +73 -0
  47. django_form_matrix-0.1.0/src/django_form_matrix/py.typed +0 -0
  48. django_form_matrix-0.1.0/src/django_form_matrix/static/django_form_matrix/css/matrix.css +145 -0
  49. django_form_matrix-0.1.0/src/django_form_matrix/static/django_form_matrix/css/unfold.css +35 -0
  50. django_form_matrix-0.1.0/src/django_form_matrix/static/django_form_matrix/js/matrix.js +67 -0
  51. django_form_matrix-0.1.0/src/django_form_matrix/templates/django_form_matrix/matrix.html +78 -0
  52. django_form_matrix-0.1.0/src/django_form_matrix/templates/django_form_matrix/permissions.html +89 -0
  53. django_form_matrix-0.1.0/src/django_form_matrix/widgets.py +148 -0
  54. django_form_matrix-0.1.0/tests/__init__.py +0 -0
  55. django_form_matrix-0.1.0/tests/conftest.py +17 -0
  56. django_form_matrix-0.1.0/tests/e2e/__init__.py +0 -0
  57. django_form_matrix-0.1.0/tests/e2e/test_e2e.py +85 -0
  58. django_form_matrix-0.1.0/tests/js/matrix.test.js +115 -0
  59. django_form_matrix-0.1.0/tests/settings.py +45 -0
  60. django_form_matrix-0.1.0/tests/test_blocks.py +74 -0
  61. django_form_matrix-0.1.0/tests/test_dynamic_permissions.py +106 -0
  62. django_form_matrix-0.1.0/tests/test_fields.py +123 -0
  63. django_form_matrix-0.1.0/tests/test_i18n.py +122 -0
  64. django_form_matrix-0.1.0/tests/test_permissions.py +152 -0
  65. django_form_matrix-0.1.0/tests/test_unfold.py +18 -0
  66. django_form_matrix-0.1.0/tests/test_widgets.py +137 -0
  67. django_form_matrix-0.1.0/tests/urls.py +6 -0
  68. django_form_matrix-0.1.0/uv.lock +1591 -0
  69. django_form_matrix-0.1.0/vitest.config.js +15 -0
@@ -0,0 +1,27 @@
1
+ name: Compatibility Matrix
2
+
3
+ on:
4
+ push:
5
+ branches: [master, main]
6
+ pull_request:
7
+ branches: [master, main]
8
+
9
+ jobs:
10
+ tox:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - name: Install uv
16
+ uses: astral-sh/setup-uv@v5
17
+ with:
18
+ enable-cache: true
19
+
20
+ - name: Install Python versions
21
+ run: uv python install 3.10 3.12 3.13 3.14
22
+
23
+ - name: Install dependencies
24
+ run: uv sync
25
+
26
+ - name: Run compatibility matrix (tox)
27
+ run: uv run tox run
@@ -0,0 +1,24 @@
1
+ name: JavaScript Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Setup Node.js
15
+ uses: actions/setup-node@v4
16
+ with:
17
+ node-version: "20"
18
+ cache: "npm"
19
+
20
+ - name: Install dependencies
21
+ run: npm ci
22
+
23
+ - name: Run Tests & Coverage
24
+ run: npm run coverage
@@ -0,0 +1,28 @@
1
+ name: Pre-commit
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+
8
+ jobs:
9
+ pre-commit:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Install uv
15
+ uses: astral-sh/setup-uv@v5
16
+ with:
17
+ enable-cache: true
18
+
19
+ # The hooks run on python3.14 (default_language_version), found
20
+ # through the venv's bin on PATH under `uv run`.
21
+ - name: Set up Python
22
+ run: uv python install 3.14
23
+
24
+ - name: Install dependencies
25
+ run: uv sync --python 3.14
26
+
27
+ - name: Run pre-commit
28
+ run: uv run pre-commit run --all-files
@@ -0,0 +1,31 @@
1
+ name: Python and Playwright Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [master, main]
6
+ pull_request:
7
+ branches: [master, main]
8
+
9
+ jobs:
10
+ test-python:
11
+ name: Python Tests
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Install uv
17
+ uses: astral-sh/setup-uv@v5
18
+ with:
19
+ enable-cache: true
20
+
21
+ - name: Set up Python
22
+ run: uv python install
23
+
24
+ - name: Install dependencies
25
+ run: uv sync
26
+
27
+ - name: Install Playwright browsers
28
+ run: uv run playwright install --with-deps chromium
29
+
30
+ - name: Run Pytest
31
+ run: uv run pytest
@@ -0,0 +1,55 @@
1
+ name: Release
2
+
3
+ on:
4
+ release:
5
+ types:
6
+ - published
7
+
8
+ jobs:
9
+ release-build:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - name: Install uv
16
+ uses: astral-sh/setup-uv@v5
17
+ with:
18
+ enable-cache: true
19
+
20
+ - name: Show project version
21
+ run: |
22
+ grep "version" pyproject.toml
23
+
24
+ - name: Build release distributions
25
+ run: uv build
26
+
27
+ - name: Upload distributions
28
+ uses: actions/upload-artifact@v4
29
+ with:
30
+ name: release-dists
31
+ path: dist/
32
+
33
+ pypi-publish:
34
+ runs-on: ubuntu-latest
35
+ needs:
36
+ - release-build
37
+ permissions:
38
+ id-token: write
39
+ contents: read
40
+
41
+ environment:
42
+ name: pypi
43
+ url: https://pypi.org/project/django-form-matrix/
44
+
45
+ steps:
46
+ - name: Retrieve release distributions
47
+ uses: actions/download-artifact@v4
48
+ with:
49
+ name: release-dists
50
+ path: dist/
51
+
52
+ - name: Publish release distributions to PyPI
53
+ uses: pypa/gh-action-pypi-publish@release/v1
54
+ with:
55
+ packages-dir: dist/
@@ -0,0 +1,42 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .installed.cfg
9
+ *.egg
10
+
11
+ # Virtual environments
12
+ .venv/
13
+ venv/
14
+ env/
15
+
16
+ # Testing / coverage
17
+ .pytest_cache/
18
+ .coverage
19
+ .coverage.*
20
+ coverage/
21
+ htmlcov/
22
+ .mypy_cache/
23
+ .ruff_cache/
24
+ .tox/
25
+
26
+ # Django
27
+ *.log
28
+ local_settings.py
29
+ db.sqlite3
30
+ db.sqlite3-journal
31
+ media/
32
+
33
+ # Node / JS
34
+ node_modules/
35
+
36
+ # Editors / OS
37
+ .vscode/
38
+ .idea/
39
+ .DS_Store
40
+
41
+ # Docs
42
+ docs/_build/
@@ -0,0 +1,66 @@
1
+ default_language_version:
2
+ python: &python-version python3.14
3
+
4
+ repos:
5
+ - repo: https://github.com/pre-commit/pre-commit-hooks
6
+ rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0
7
+ hooks:
8
+ - id: check-added-large-files
9
+ - id: check-case-conflict
10
+ - id: check-json
11
+ - id: check-merge-conflict
12
+ - id: check-symlinks
13
+ - id: check-toml
14
+ - id: debug-statements
15
+ - id: end-of-file-fixer
16
+ - id: trailing-whitespace
17
+
18
+ - repo: https://github.com/biomejs/pre-commit
19
+ rev: d1ce4972fb5ad09afd33b432bca71e8c5dfbb2b5 # frozen: v2.5.6
20
+ hooks:
21
+ - id: biome-check
22
+ additional_dependencies:
23
+ - '@biomejs/biome@2.3.8'
24
+ language_version: default
25
+
26
+ - repo: https://github.com/adamchainz/django-upgrade
27
+ rev: bdcc9646c249f00ec9781751791e7b75b48b3722 # frozen: 1.31.1
28
+ hooks:
29
+ - id: django-upgrade
30
+ args: [--target-version, "4.2"]
31
+
32
+ - repo: https://github.com/astral-sh/ruff-pre-commit
33
+ rev: 39d9ac5938dadb73df0564a45f163e25ff9fa6e2 # frozen: v0.16.1
34
+ hooks:
35
+ - id: ruff-check
36
+ args: [--fix]
37
+ - id: ruff-format
38
+
39
+ - repo: local
40
+ hooks:
41
+ - id: mypy
42
+ name: mypy
43
+ entry: mypy
44
+ language: python
45
+ additional_dependencies:
46
+ - mypy>=1.10
47
+ - django-stubs[compatible-mypy]>=5
48
+ - django>=4.2
49
+ - pytest-django>=4.8
50
+ types: [python]
51
+ files: ^(src|example)/
52
+ pass_filenames: false
53
+ args: [src/django_form_matrix, example]
54
+
55
+ # Only needed if the library ships Django templates (src/<pkg>/templates/).
56
+ # Remove this hook if it doesn't.
57
+ - repo: https://github.com/adamchainz/djade-pre-commit
58
+ rev: 52a7ce253113456c2a1113186a4a19b15c8b68af # frozen: 1.9.0
59
+ hooks:
60
+ - id: djade
61
+ files: '(/|^)templates/'
62
+
63
+ - repo: https://github.com/tox-dev/pyproject-fmt
64
+ rev: d600c142bb19f521ae6a6a345f94a2efd7937759 # frozen: v2.26.0
65
+ hooks:
66
+ - id: pyproject-fmt
@@ -0,0 +1,21 @@
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.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-10-17
9
+
10
+ ### Added
11
+
12
+ - `MatrixField`, `MatrixWidget`, `Row` and `Column`: a table of rows and
13
+ columns with a form field in every cell, optional collapsible sections and
14
+ shortcuts to check a row, a column or a section.
15
+ - `django_form_matrix.contrib.permissions`: `PermissionMultipleChoiceField`,
16
+ the permissions of an app/model/action in a matrix.
17
+ - Blocks in `matrix.html` and `permissions.html` (`section_summary`,
18
+ `column_heading`, `cell`...) to extend them changing only a piece.
19
+ - `django_form_matrix.contrib.unfold.UnfoldMatrixMixin`: the matrix on
20
+ django-unfold's palette (light and dark).
21
+ - Spanish translation.
@@ -0,0 +1,113 @@
1
+ # Contributing
2
+
3
+ ## Development setup
4
+
5
+ With [uv](https://docs.astral.sh/uv/) (recommended):
6
+
7
+ ```bash
8
+ uv sync
9
+ uv run playwright install --with-deps chromium
10
+ npm install
11
+ uv run pre-commit install
12
+ ```
13
+
14
+ Without uv:
15
+
16
+ ```bash
17
+ pip install -e ".[dev]"
18
+ playwright install --with-deps chromium
19
+ npm install
20
+ pre-commit install
21
+ ```
22
+
23
+ ## Running the example project
24
+
25
+ ```bash
26
+ cd example
27
+ python manage.py migrate
28
+ python manage.py createsuperuser
29
+ python manage.py runserver
30
+ ```
31
+
32
+ ## Tests
33
+
34
+ Python (pytest + pytest-django, coverage always on via `addopts`):
35
+
36
+ ```bash
37
+ uv run pytest
38
+ ```
39
+
40
+ `tests/e2e/` (real-browser tests via pytest-playwright, driving a real
41
+ Chromium against `live_server`) runs as part of that same `uv run pytest`
42
+ — no separate command. They need `uv run playwright install chromium`
43
+ once (see Setup above) and are slower than the rest of the suite;
44
+ everything else under `tests/` is unit/integration-level and doesn't
45
+ touch a browser.
46
+
47
+ JavaScript (vitest + jsdom):
48
+
49
+ ```bash
50
+ npm test
51
+ npm run coverage
52
+ ```
53
+
54
+ ## Compatibility matrix (tox)
55
+
56
+ `uv run pytest` above only runs against whatever Django version `uv.lock`
57
+ resolved (the newest one satisfying `dependencies`). To check the full
58
+ supported range — every Django series in `classifiers`, against the
59
+ oldest and newest Python it supports (within this package's own
60
+ `requires-python` floor) — run the tox matrix instead:
61
+
62
+ ```bash
63
+ uv run tox run-parallel # every env, in parallel
64
+ uv run tox -e py314-dj52 # a single env, e.g. to debug one failure
65
+ ```
66
+
67
+ `[tool.tox]` in `pyproject.toml` lists the exact envs. Each one gets its own ephemeral venv (via
68
+ [tox-uv](https://github.com/tox-dev/tox-uv), using uv's own Python
69
+ builds — `uv python install <version>` once for any you don't have yet)
70
+ with only `pytest`/`pytest-django`/`pytest-cov` and that env's pinned
71
+ Django, not the full `dev` group, and skips `tests/e2e/` (no Playwright
72
+ in those envs). This only tests boundaries (oldest + newest Python per
73
+ Django series), not every valid combination — that catches most real
74
+ breakage while staying fast. Runs in CI as a separate
75
+ `compat-matrix.yml` workflow, alongside the regular `pytest.yml`. Update
76
+ the `env_list` here whenever `classifiers` gains or drops a Django
77
+ series.
78
+
79
+ ## Lint / format / type-check
80
+
81
+ ```bash
82
+ uv run ruff check .
83
+ uv run ruff format --check .
84
+ uv run mypy src example
85
+ # or, everything at once:
86
+ uv run pre-commit run --all-files
87
+ ```
88
+
89
+ The mypy pre-commit hook runs against the project venv (it needs
90
+ django-stubs to resolve model types), not an isolated pre-commit env.
91
+
92
+ ## Before opening a PR
93
+
94
+ - [ ] `uv run pytest`
95
+ - [ ] `npm test`
96
+ - [ ] `uv run pre-commit run --all-files`
97
+
98
+ ## Translations
99
+
100
+ Catalogs live in `src/django_form_matrix/locale/`. After changing
101
+ translatable text, from that package directory:
102
+
103
+ ```bash
104
+ cd src/django_form_matrix
105
+ DJANGO_SETTINGS_MODULE=tests.settings PYTHONPATH=../..:../../example \
106
+ uv run django-admin makemessages -l es --no-obsolete --add-location file --no-wrap
107
+ # translate the new/changed entries in locale/es/LC_MESSAGES/django.po, then:
108
+ DJANGO_SETTINGS_MODULE=tests.settings PYTHONPATH=../..:../../example \
109
+ uv run django-admin compilemessages
110
+ ```
111
+
112
+ Commit both the `.po` and the compiled `.mo`. `tests/test_i18n.py` fails if
113
+ an entry is untranslated or fuzzy, or if the `.mo` doesn't match the `.po`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rodolfo Becerra
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.
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.5
2
+ Name: django-form-matrix
3
+ Version: 0.1.0
4
+ Summary: A form field to fill a table of rows and columns, with a form field in every cell (checkboxes, numbers, choices...), plus a ready-made permission matrix.
5
+ Project-URL: Homepage, https://github.com/rodolvbg/django-form-matrix
6
+ Project-URL: Issues, https://github.com/rodolvbg/django-form-matrix/issues
7
+ Project-URL: Repository, https://github.com/rodolvbg/django-form-matrix
8
+ Author-email: Rodolfo Valentín Becerra García <rodolvbg@gmail.com>
9
+ Maintainer-email: Rodolfo Valentín Becerra García <rodolvbg@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: admin,checkbox,django,form,grid,matrix,permissions,widget
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Web Environment
15
+ Classifier: Framework :: Django
16
+ Classifier: Framework :: Django :: 4.2
17
+ Classifier: Framework :: Django :: 5.0
18
+ Classifier: Framework :: Django :: 5.1
19
+ Classifier: Framework :: Django :: 5.2
20
+ Classifier: Intended Audience :: Developers
21
+ Classifier: License :: OSI Approved :: MIT License
22
+ Classifier: Operating System :: OS Independent
23
+ Classifier: Programming Language :: Python :: 3 :: Only
24
+ Classifier: Programming Language :: Python :: 3.10
25
+ Classifier: Programming Language :: Python :: 3.11
26
+ Classifier: Programming Language :: Python :: 3.12
27
+ Classifier: Programming Language :: Python :: 3.13
28
+ Classifier: Programming Language :: Python :: 3.14
29
+ Classifier: Typing :: Typed
30
+ Requires-Python: >=3.10
31
+ Requires-Dist: django>=4.2
32
+ Provides-Extra: dev
33
+ Requires-Dist: coverage[toml]>=7.4; extra == 'dev'
34
+ Requires-Dist: django-stubs[compatible-mypy]>=5; extra == 'dev'
35
+ Requires-Dist: django-unfold>=0.60; extra == 'dev'
36
+ Requires-Dist: mypy>=1.10; extra == 'dev'
37
+ Requires-Dist: pre-commit>=4; extra == 'dev'
38
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
39
+ Requires-Dist: pytest-django>=4.8; extra == 'dev'
40
+ Requires-Dist: pytest-playwright>=0.5; extra == 'dev'
41
+ Requires-Dist: pytest>=8; extra == 'dev'
42
+ Requires-Dist: ruff>=0.6; extra == 'dev'
43
+ Requires-Dist: tox-uv>=1; extra == 'dev'
44
+ Requires-Dist: tox>=4; extra == 'dev'
45
+ Description-Content-Type: text/markdown
46
+
47
+ # django-form-matrix
48
+
49
+ [![Build status](https://github.com/rodolvbg/django-form-matrix/actions/workflows/pytest.yml/badge.svg)](https://github.com/rodolvbg/django-form-matrix/actions/workflows/pytest.yml)
50
+ [![PyPI version](https://img.shields.io/pypi/v/django-form-matrix.svg)](https://pypi.org/project/django-form-matrix/)
51
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-form-matrix)](https://pypi.org/project/django-form-matrix/)
52
+ [![PyPI - Django Version](https://img.shields.io/pypi/djversions/django-form-matrix)](https://pypi.org/project/django-form-matrix/)
53
+ [![Downloads](https://static.pepy.tech/personalized-badge/django-form-matrix?period=month&units=international_system&left_color=black&right_color=blue&left_text=Downloads/month)](https://pepy.tech/project/django-form-matrix)
54
+
55
+ A Django form field to fill a **table**: one row per item, one column per property and, in every cell, a form field of its own — a checkbox, a number, a choice, text. Rows can be grouped in collapsible sections, and checkbox columns get shortcuts to check a row, a column or a whole section.
56
+
57
+ It works in any Django form and in the admin. A ready-made **permission matrix** (apps → models → add/view/change/delete) is included as a use case.
58
+
59
+ ![A matrix with checkboxes, numbers and choices in its cells, in two sections](https://raw.githubusercontent.com/rodolvbg/django-form-matrix/HEAD/docs/screenshots/hero.png)
60
+
61
+ ## Installation
62
+
63
+ ```bash
64
+ pip install django-form-matrix
65
+ ```
66
+
67
+ Add `django_form_matrix` to `INSTALLED_APPS` (it ships a template, a script and a stylesheet):
68
+
69
+ ```python
70
+ INSTALLED_APPS = [
71
+ # ...
72
+ "django_form_matrix",
73
+ ]
74
+ ```
75
+
76
+ ## Quick start
77
+
78
+ ```python
79
+ from django import forms
80
+ from django_form_matrix import Column, MatrixField, Row
81
+
82
+
83
+ class PlanForm(forms.Form):
84
+ features = MatrixField(
85
+ rows=[
86
+ Row("invoices", "Invoices", section="Billing"),
87
+ Row("quotes", "Quotes", section="Billing"),
88
+ Row("exports", "Exports", section="Analytics", columns=["enabled"]),
89
+ ],
90
+ columns=[
91
+ Column("enabled", "Enabled"), # a checkbox unless said otherwise
92
+ Column(
93
+ "limit",
94
+ "Monthly limit",
95
+ forms.IntegerField(min_value=0, required=False),
96
+ ),
97
+ ],
98
+ )
99
+ ```
100
+
101
+ Render the form as usual and add `{{ form.media }}` (the admin does it for you). The cleaned value has one entry per row with the cells that exist:
102
+
103
+ ```python
104
+ form.cleaned_data["features"]
105
+ # {"invoices": {"enabled": True, "limit": 50},
106
+ # "quotes": {"enabled": False, "limit": None},
107
+ # "exports": {"enabled": False}}
108
+ ```
109
+
110
+ That is what a `JSONField` stores, so a `ModelForm` can save it directly.
111
+
112
+ ### Rows and columns
113
+
114
+ - `Column(key, label, field=BooleanField(required=False))`: the form field of the column. Its validation and its widget are Django's own, and a failing cell is reported with its row and column ("Invoices, Monthly limit: …").
115
+ - `Row(key, label, section=None, columns=None)`: `section` groups rows under a collapsible heading (rows without one go in a plain table); `columns` are the keys of the columns that have a cell in this row, the rest show a dash.
116
+ - `MatrixField(rows=…, columns=…)` is not required by default; with `required=True` at least one cell needs a value. `rows` and `columns` can be reassigned in a form's `__init__` to build the matrix dynamically.
117
+
118
+ Each cell is submitted as `<name>__<row key>__<column key>`, so it reads like any other widget.
119
+
120
+ ### Shortcuts
121
+
122
+ Checkbox columns get a checkbox in their heading, every row with checkboxes gets one at its start, and each section gets an "All". They show whether their cells are all, some or none checked, and the heading of a section counts the checked ones. A section opens by itself when something in it is filled.
123
+
124
+ ## Permissions
125
+
126
+ ```python
127
+ from django_form_matrix.contrib.permissions import PermissionMultipleChoiceField
128
+
129
+
130
+ class RoleAdmin(admin.ModelAdmin):
131
+ def formfield_for_manytomany(self, db_field, request, **kwargs):
132
+ if db_field.name == "permissions":
133
+ kwargs["form_class"] = PermissionMultipleChoiceField
134
+ return super().formfield_for_manytomany(db_field, request, **kwargs)
135
+ ```
136
+
137
+ ![The permission matrix in the admin](https://raw.githubusercontent.com/rodolvbg/django-form-matrix/HEAD/docs/screenshots/permissions.png)
138
+
139
+ One section per app, one row per model and a column per action; the other permissions of a model go in an "Others" column. It submits like a `CheckboxSelectMultiple`, so validation and saving do not change, and the names of the permissions are shown translated. Needs `django.contrib.auth`. [docs/examples/permissions.md](https://github.com/rodolvbg/django-form-matrix/blob/HEAD/docs/examples/permissions.md) shows how to build the same matrix by hand with `MatrixField`, with rows that come from the database, for when you need more control.
140
+
141
+ ## Customizing the HTML
142
+
143
+ Both templates are split in blocks, so a template of yours can extend one and change only a piece, without copying the rest:
144
+
145
+ ```django
146
+ {% extends "django_form_matrix/matrix.html" %}
147
+ {% block section_summary %}<strong>{{ section.name }}</strong>{% endblock %}
148
+ {% block cell %}<div class="my-cell">{{ cell.html }}</div>{% endblock %}
149
+ ```
150
+
151
+ Point a widget to it with `template_name`:
152
+
153
+ ```python
154
+ class MyMatrixWidget(MatrixWidget):
155
+ template_name = "myapp/my_matrix.html"
156
+ ```
157
+
158
+ | Block | `matrix.html` | `permissions.html` | What it is |
159
+ | --- | --- | --- | --- |
160
+ | `section_summary` | ✓ | ✓ | The heading of a section (its name and count) |
161
+ | `section_toggle` | ✓ | ✓ | The "All" checkbox of a section |
162
+ | `column_heading` | ✓ | ✓ | The heading of a column |
163
+ | `others_heading` | | ✓ | The heading of the "Others" column |
164
+ | `row_heading` | ✓ | ✓ | The heading of a row |
165
+ | `cell` | ✓ | | The widget of a cell (`{{ cell.html }}`) |
166
+ | `permission` | | ✓ | The checkbox of a permission |
167
+ | `other_permission` | | ✓ | A permission of the "Others" column |
168
+ | `missing_cell` | ✓ | ✓ | A cell a row does not have |
169
+ | `empty` | ✓ | ✓ | What shows when there is nothing to fill |
170
+
171
+ `{{ block.super }}` keeps the original of a block. The classes (`.form-matrix-*`) and the `data-form-matrix-*` attributes are what the stylesheet and the script look for: keep them in your blocks and the shortcuts keep working.
172
+
173
+ Django looks for the widget templates of a form in the `templates/` of the installed apps, **not** in the `DIRS` of your project. To override `django_form_matrix/matrix.html` itself by name from the project's templates, set `FORM_RENDERER = "django.forms.renderers.TemplatesSetting"`, or put the template in an app listed before `django_form_matrix` in `INSTALLED_APPS`. Extending it with your own `template_name`, as above, needs neither.
174
+
175
+ ## Styling
176
+
177
+ The stylesheet uses the colours of the Django admin when it is there. Anywhere else, set `--form-matrix-border`, `--form-matrix-heading-bg` and `--form-matrix-quiet` on `.form-matrix`.
178
+
179
+ ### Unfold
180
+
181
+ [django-unfold](https://github.com/unfoldadmin/django-unfold) does not define the variables of Django's admin, so add `UnfoldMatrixMixin` to the admin: it puts the matrix on Unfold's palette, light and dark, and styles the number, text and select cells like Unfold's own inputs.
182
+
183
+ ```python
184
+ from unfold.admin import ModelAdmin
185
+ from django_form_matrix.contrib.unfold import UnfoldMatrixMixin
186
+
187
+
188
+ @admin.register(Plan)
189
+ class PlanAdmin(UnfoldMatrixMixin, ModelAdmin):
190
+ form = PlanForm
191
+ ```
192
+
193
+ ![The matrix in Unfold](https://raw.githubusercontent.com/rodolvbg/django-form-matrix/HEAD/docs/screenshots/unfold.png)
194
+
195
+ The permission matrix, the use case this library started from, in Unfold:
196
+
197
+ ![The permission matrix in Unfold](https://raw.githubusercontent.com/rodolvbg/django-form-matrix/HEAD/docs/screenshots/unfold-permissions.png)
198
+
199
+ And in Unfold's dark mode:
200
+
201
+ ![The matrix in Unfold, dark](https://raw.githubusercontent.com/rodolvbg/django-form-matrix/HEAD/docs/screenshots/unfold-dark.png)
202
+
203
+ ## Translations
204
+
205
+ Ships a Spanish (`es`) translation: the matrix's headings ("Add", "View",
206
+ "Change", "Delete", "Others"), the permissions' names ("Can view plan") and
207
+ its messages follow the active language, like the rest of the admin.
208
+
209
+ ## Development
210
+
211
+ ```bash
212
+ uv sync && npm install
213
+ uv run pytest # Python + end-to-end (needs `uv run playwright install chromium`)
214
+ npm test # JS
215
+ cd example && uv run python manage.py runserver # demo: Plans and Roles in the admin
216
+ DJANGO_SETTINGS_MODULE=example.settings_unfold uv run python manage.py runserver # the same, in Unfold
217
+ ```