django-modelsheet 0.3.0a1__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 (96) hide show
  1. django_modelsheet-0.3.0a1/CHANGELOG.md +13 -0
  2. django_modelsheet-0.3.0a1/CONTRIBUTING.md +18 -0
  3. django_modelsheet-0.3.0a1/LICENSE +21 -0
  4. django_modelsheet-0.3.0a1/MANIFEST.in +13 -0
  5. django_modelsheet-0.3.0a1/PKG-INFO +196 -0
  6. django_modelsheet-0.3.0a1/README.md +164 -0
  7. django_modelsheet-0.3.0a1/SECURITY.md +15 -0
  8. django_modelsheet-0.3.0a1/demo/__init__.py +0 -0
  9. django_modelsheet-0.3.0a1/demo/settings.py +76 -0
  10. django_modelsheet-0.3.0a1/demo/shop/__init__.py +0 -0
  11. django_modelsheet-0.3.0a1/demo/shop/admin.py +46 -0
  12. django_modelsheet-0.3.0a1/demo/shop/apps.py +6 -0
  13. django_modelsheet-0.3.0a1/demo/shop/management/__init__.py +0 -0
  14. django_modelsheet-0.3.0a1/demo/shop/management/commands/__init__.py +0 -0
  15. django_modelsheet-0.3.0a1/demo/shop/management/commands/seed_demo.py +38 -0
  16. django_modelsheet-0.3.0a1/demo/shop/migrations/0001_initial.py +75 -0
  17. django_modelsheet-0.3.0a1/demo/shop/migrations/__init__.py +0 -0
  18. django_modelsheet-0.3.0a1/demo/shop/models.py +39 -0
  19. django_modelsheet-0.3.0a1/demo/urls.py +4 -0
  20. django_modelsheet-0.3.0a1/django_modelsheet.egg-info/PKG-INFO +196 -0
  21. django_modelsheet-0.3.0a1/django_modelsheet.egg-info/SOURCES.txt +94 -0
  22. django_modelsheet-0.3.0a1/django_modelsheet.egg-info/dependency_links.txt +1 -0
  23. django_modelsheet-0.3.0a1/django_modelsheet.egg-info/requires.txt +15 -0
  24. django_modelsheet-0.3.0a1/django_modelsheet.egg-info/top_level.txt +1 -0
  25. django_modelsheet-0.3.0a1/docs/api.md +52 -0
  26. django_modelsheet-0.3.0a1/docs/background.md +52 -0
  27. django_modelsheet-0.3.0a1/docs/benchmark-100k.json +36 -0
  28. django_modelsheet-0.3.0a1/docs/benchmark-1m.json +36 -0
  29. django_modelsheet-0.3.0a1/docs/benchmarks.md +17 -0
  30. django_modelsheet-0.3.0a1/docs/extensions.md +52 -0
  31. django_modelsheet-0.3.0a1/docs/formulas.md +46 -0
  32. django_modelsheet-0.3.0a1/docs/releasing.md +57 -0
  33. django_modelsheet-0.3.0a1/docs/validation.md +76 -0
  34. django_modelsheet-0.3.0a1/frontend/e2e/sheet.spec.ts +167 -0
  35. django_modelsheet-0.3.0a1/frontend/package-lock.json +1707 -0
  36. django_modelsheet-0.3.0a1/frontend/package.json +23 -0
  37. django_modelsheet-0.3.0a1/frontend/playwright.config.ts +13 -0
  38. django_modelsheet-0.3.0a1/frontend/src/buffer.ts +102 -0
  39. django_modelsheet-0.3.0a1/frontend/src/formula.ts +260 -0
  40. django_modelsheet-0.3.0a1/frontend/src/i18n.ts +24 -0
  41. django_modelsheet-0.3.0a1/frontend/src/main.ts +495 -0
  42. django_modelsheet-0.3.0a1/frontend/src/style.css +9 -0
  43. django_modelsheet-0.3.0a1/frontend/tests/buffer.test.ts +43 -0
  44. django_modelsheet-0.3.0a1/frontend/tests/formula.test.ts +22 -0
  45. django_modelsheet-0.3.0a1/frontend/tsconfig.json +1 -0
  46. django_modelsheet-0.3.0a1/frontend/vite.config.ts +12 -0
  47. django_modelsheet-0.3.0a1/frontend/vitest.config.ts +3 -0
  48. django_modelsheet-0.3.0a1/manage.py +8 -0
  49. django_modelsheet-0.3.0a1/modelsheet/__init__.py +1 -0
  50. django_modelsheet-0.3.0a1/modelsheet/adapters.py +121 -0
  51. django_modelsheet-0.3.0a1/modelsheet/admin.py +325 -0
  52. django_modelsheet-0.3.0a1/modelsheet/apps.py +14 -0
  53. django_modelsheet-0.3.0a1/modelsheet/background.py +44 -0
  54. django_modelsheet-0.3.0a1/modelsheet/bulk.py +410 -0
  55. django_modelsheet-0.3.0a1/modelsheet/checks.py +90 -0
  56. django_modelsheet-0.3.0a1/modelsheet/conf.py +42 -0
  57. django_modelsheet-0.3.0a1/modelsheet/errors.py +18 -0
  58. django_modelsheet-0.3.0a1/modelsheet/formula/__init__.py +3 -0
  59. django_modelsheet-0.3.0a1/modelsheet/formula/engine.py +392 -0
  60. django_modelsheet-0.3.0a1/modelsheet/locale/ru/LC_MESSAGES/django.mo +0 -0
  61. django_modelsheet-0.3.0a1/modelsheet/locale/ru/LC_MESSAGES/django.po +8 -0
  62. django_modelsheet-0.3.0a1/modelsheet/locale/ru/LC_MESSAGES/djangojs.mo +0 -0
  63. django_modelsheet-0.3.0a1/modelsheet/locale/ru/LC_MESSAGES/djangojs.po +188 -0
  64. django_modelsheet-0.3.0a1/modelsheet/management/__init__.py +0 -0
  65. django_modelsheet-0.3.0a1/modelsheet/management/commands/__init__.py +0 -0
  66. django_modelsheet-0.3.0a1/modelsheet/management/commands/modelsheet_cleanup.py +24 -0
  67. django_modelsheet-0.3.0a1/modelsheet/management/commands/modelsheet_roles.py +47 -0
  68. django_modelsheet-0.3.0a1/modelsheet/metadata.py +42 -0
  69. django_modelsheet-0.3.0a1/modelsheet/migrations/0001_initial.py +114 -0
  70. django_modelsheet-0.3.0a1/modelsheet/migrations/__init__.py +0 -0
  71. django_modelsheet-0.3.0a1/modelsheet/models.py +63 -0
  72. django_modelsheet-0.3.0a1/modelsheet/permissions.py +72 -0
  73. django_modelsheet-0.3.0a1/modelsheet/py.typed +0 -0
  74. django_modelsheet-0.3.0a1/modelsheet/query.py +133 -0
  75. django_modelsheet-0.3.0a1/modelsheet/services.py +490 -0
  76. django_modelsheet-0.3.0a1/modelsheet/static/modelsheet/THIRD_PARTY_LICENSES.txt +49 -0
  77. django_modelsheet-0.3.0a1/modelsheet/static/modelsheet/sheet.css +1 -0
  78. django_modelsheet-0.3.0a1/modelsheet/static/modelsheet/sheet.js +13482 -0
  79. django_modelsheet-0.3.0a1/modelsheet/templates/modelsheet/audit.html +14 -0
  80. django_modelsheet-0.3.0a1/modelsheet/templates/modelsheet/change_list.html +6 -0
  81. django_modelsheet-0.3.0a1/modelsheet/templates/modelsheet/sheet.html +11 -0
  82. django_modelsheet-0.3.0a1/modelsheet/templates/modelsheet/unavailable.html +2 -0
  83. django_modelsheet-0.3.0a1/modelsheet/views.py +91 -0
  84. django_modelsheet-0.3.0a1/pyproject.toml +49 -0
  85. django_modelsheet-0.3.0a1/scripts/benchmark.py +116 -0
  86. django_modelsheet-0.3.0a1/scripts/check_pypi_name.py +25 -0
  87. django_modelsheet-0.3.0a1/scripts/package_smoke.py +54 -0
  88. django_modelsheet-0.3.0a1/setup.cfg +4 -0
  89. django_modelsheet-0.3.0a1/tests/__init__.py +0 -0
  90. django_modelsheet-0.3.0a1/tests/conftest.py +65 -0
  91. django_modelsheet-0.3.0a1/tests/formula_cases.json +51 -0
  92. django_modelsheet-0.3.0a1/tests/test_api.py +311 -0
  93. django_modelsheet-0.3.0a1/tests/test_bulk.py +209 -0
  94. django_modelsheet-0.3.0a1/tests/test_concurrency.py +62 -0
  95. django_modelsheet-0.3.0a1/tests/test_configuration.py +102 -0
  96. django_modelsheet-0.3.0a1/tests/test_formula.py +57 -0
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0a1 — 2026-10-24
4
+
5
+ - Initial Django package, explicit Admin registration, precompiled static assets.
6
+ - Spreadsheet range editing, local undo/redo, safe atomic saves and row fingerprints.
7
+ - Permission-aware search, grouped filters, sorting, snapshots and Admin switching.
8
+ - Bounded Python/TypeScript formula engines with shared golden fixtures.
9
+ - Immutable bulk previews, confirmation, audit, idempotency and background adapter contract.
10
+ - Demo project, migrations, role/cleanup commands, CI and packaging checks.
11
+
12
+ This initial release combines the planned v0.1–v0.3 increments. There are no prior
13
+ published schemas to migrate; future releases must preserve forward migration paths.
@@ -0,0 +1,18 @@
1
+ # Contributing
2
+
3
+ Use Python 3.11+ (3.12+ with Django 6.x) and Node 22. Install `.[test]`, then run
4
+ `npm ci` inside frontend. Build assets with `npm run build` and include both source
5
+ and generated assets in changes. Never require npm in consuming Django projects.
6
+
7
+ Run pytest, ruff, Vitest and Playwright as described in README. Formula semantics
8
+ changes must add fixtures to tests/formula_cases.json and pass both runtimes.
9
+ Use actual transactional tests with separate connections for concurrency changes.
10
+ Database-specific behavior must be exercised by the PostgreSQL/MySQL CI jobs.
11
+
12
+ Run makemigrations for schema changes. Do not rewrite released migrations. Preserve
13
+ permission checks on every API/worker entry point and safe saves by default.
14
+ Do not silently broaden a preview's members or automatically replay failed items.
15
+
16
+ Update docs and CHANGELOG for public interfaces. Add dependency license notices when
17
+ bundling new assets. Avoid committing local credentials, demo databases and generated
18
+ test reports. The demo's explicit test credentials must not be used in production.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 django-modelsheet contributors
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,13 @@
1
+ include LICENSE README.md CHANGELOG.md CONTRIBUTING.md SECURITY.md
2
+ recursive-include modelsheet/static *
3
+ recursive-include modelsheet/templates *.html
4
+ recursive-include modelsheet/locale *.po *.mo
5
+ recursive-include docs *.md *.json
6
+ recursive-include frontend *.ts *.json *.html *.css
7
+ prune frontend/node_modules
8
+ prune frontend/test-results
9
+ prune frontend/playwright-report
10
+ recursive-include tests *.py *.json
11
+ recursive-include demo *.py
12
+ recursive-include scripts *.py
13
+ include manage.py
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-modelsheet
3
+ Version: 0.3.0a1
4
+ Summary: Spreadsheet editing, formulas and previewed bulk operations for Django Admin
5
+ License-Expression: MIT
6
+ Project-URL: Repository, https://github.com/svalench/django-modelsheet
7
+ Project-URL: Documentation, https://github.com/svalench/django-modelsheet/blob/main/README.md
8
+ Project-URL: Issues, https://github.com/svalench/django-modelsheet/issues
9
+ Project-URL: Changelog, https://github.com/svalench/django-modelsheet/blob/main/CHANGELOG.md
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: Django
12
+ Classifier: Framework :: Django :: 5.2
13
+ Classifier: Framework :: Django :: 6.0
14
+ Classifier: Framework :: Django :: 6.1
15
+ Classifier: Programming Language :: Python :: 3
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: Django<6.2,>=5.2
20
+ Provides-Extra: test
21
+ Requires-Dist: pytest>=8; extra == "test"
22
+ Requires-Dist: pytest-django>=4.9; extra == "test"
23
+ Requires-Dist: pytest-timeout>=2; extra == "test"
24
+ Requires-Dist: build; extra == "test"
25
+ Requires-Dist: ruff; extra == "test"
26
+ Requires-Dist: playwright; extra == "test"
27
+ Provides-Extra: postgres
28
+ Requires-Dist: psycopg[binary]>=3.2; extra == "postgres"
29
+ Provides-Extra: mysql
30
+ Requires-Dist: mysqlclient>=2.2; extra == "mysql"
31
+ Dynamic: license-file
32
+
33
+ # django-modelsheet
34
+
35
+ Spreadsheet editing for explicitly registered Django Admin models: cell/range editing,
36
+ local undo/redo, Decimal formulas, server-side filters, and previewed bulk changes.
37
+
38
+ **0.3.0a1 is an alpha.** The repository includes a working demo, compiled assets,
39
+ tests, and a compatibility CI matrix. A published PyPI release is not implied.
40
+
41
+ ## Quickstart
42
+
43
+ Install a built wheel (or `pip install .` from this repository). After publication:
44
+
45
+ ```sh
46
+ pip install django-modelsheet
47
+ ```
48
+
49
+ ```python
50
+ # settings.py
51
+ INSTALLED_APPS += ["modelsheet"]
52
+ MODELSHEET = {"models": ["shop.Product"], "page_size": 200}
53
+
54
+ # SQLite only: required for safe writes; PostgreSQL/MySQL use row locks.
55
+ DATABASES["default"].setdefault("OPTIONS", {})["transaction_mode"] = "IMMEDIATE"
56
+ ```
57
+
58
+ ```python
59
+ # admin.py
60
+ from django.contrib import admin
61
+ from modelsheet.admin import ModelSheetAdmin
62
+ from .models import Product
63
+
64
+ @admin.register(Product)
65
+ class ProductAdmin(ModelSheetAdmin):
66
+ list_display = ("name", "price", "stock")
67
+ search_fields = ("name",)
68
+ sheet_fields = ("name", "price", "stock")
69
+ sheet_editable = ("price", "stock")
70
+ sheet_computed_fields = {"inventory_value": "=[price]*[stock]"}
71
+ ```
72
+
73
+ ```sh
74
+ python manage.py migrate
75
+ python manage.py collectstatic --noinput
76
+ python manage.py modelsheet_roles # optional: creates groups, assigns no users
77
+ ```
78
+
79
+ Assign a group through Django Admin and mark the account as staff. Superusers can
80
+ use configured sheets immediately. Open **Spreadsheet** from the model list, or
81
+ visit `/admin/shop/product/sheet/`. No frontend build, CDN or extra URL include is
82
+ required in the consuming project. Admin's existing sessions and CSRF protection apply.
83
+
84
+ ## Using the sheet
85
+
86
+ - Double-click to edit; use the formula bar for a value or expression.
87
+ - Changes stay local until **Save**. Ctrl/Cmd+Z, Ctrl/Cmd+Y and Ctrl/Cmd+S operate on this buffer.
88
+ - Select a range, copy/paste TSV from Excel, or press Ctrl/Cmd+D to fill down.
89
+ - Readonly cells are skipped during paste with a notification. A save is atomic across all changed rows.
90
+ - Use **Sort** for ordered sort fields, or Alt+click a column header (Alt+Shift+click adds a field).
91
+ Ordinary header clicks select columns. Search and filters run on the server.
92
+ - **Bulk change** selects either highlighted page rows or every matching row, including other pages.
93
+ Build a preview, inspect its exact count/values, then confirm it. Editing inputs requires a new preview.
94
+ - **Ordinary Admin** is available only when its list can represent the same filter, ordering and page.
95
+
96
+ Numbers use a decimal point and dates use ISO 8601. Empty pasted cells become
97
+ NULL for nullable fields, otherwise empty text for text fields or a validation error.
98
+ Prefix text beginning with `=` with an apostrophe, for example `'=SUM(A1:A2)`.
99
+
100
+ ## Configuration
101
+
102
+ ```python
103
+ MODELSHEET = {
104
+ "models": ["shop.Product"],
105
+ "page_size": 200,
106
+ "max_page_size": 500,
107
+ "max_save_rows": 500,
108
+ "count_mode": "exact", # exact | none (no approximate COUNT promise)
109
+ "bulk_background_threshold": 5000,
110
+ "max_bulk_rows": 100000,
111
+ "chunk_size": 200,
112
+ "snapshot_ttl_seconds": 1800,
113
+ "preview_ttl_seconds": 1800,
114
+ "history_retention_days": 365,
115
+ "max_operations_per_minute": 30,
116
+ "max_request_bytes": 1000000,
117
+ "formula": {"max_length": 500, "max_depth": 10, "max_steps": 10000, "timeout_ms": 250},
118
+ "background_adapter": None,
119
+ }
120
+ ```
121
+
122
+ Run `python manage.py modelsheet_cleanup` periodically through your scheduler.
123
+ It removes expired snapshots and terminal/unused-preview operations beyond retention.
124
+ Do not reuse old idempotency keys after their operation has been purged.
125
+
126
+ ## Permissions
127
+
128
+ Access requires `is_active`, `is_staff`, `modelsheet.open`, the model's standard
129
+ `view` permission, and `ModelAdmin.has_view_permission()`. Editing additionally
130
+ requires the standard `change` permission and `has_change_permission()`.
131
+
132
+ Package permission codenames include app/model identifiers:
133
+
134
+ ```text
135
+ modelsheet.view_field_shop_product_price
136
+ modelsheet.edit_field_shop_product_price
137
+ modelsheet.bulk_shop_product
138
+ modelsheet.history_shop_product
139
+ ```
140
+
141
+ `get_queryset(request)` bounds every read/write. Object-level Admin hooks and the
142
+ configured policy apply in addition. Hidden fields cannot be used in filters,
143
+ sorting, formulas or history. Field permissions never grant model-level editing.
144
+
145
+ Group templates are opt-in and additive: Viewer, Editor, Operator, Auditor and Admin.
146
+ They grant access to the configured sheet fields; review these grants before
147
+ assigning users, especially sensitive columns. They do not grant superuser status.
148
+
149
+ ## Development and demo
150
+
151
+ ```sh
152
+ python -m pip install -e '.[test]'
153
+ cd frontend
154
+ npm ci
155
+ npm run build
156
+ npm test
157
+ cd ..
158
+ python manage.py migrate
159
+ python manage.py seed_demo --create-user
160
+ python manage.py runserver
161
+ ```
162
+
163
+ The explicit `--create-user` option creates a local-only `admin` / `admin` account.
164
+ The demo settings are not production settings. Open
165
+ `http://127.0.0.1:8000/admin/shop/product/sheet/`.
166
+
167
+ ```sh
168
+ python -m pytest
169
+ python -m ruff check .
170
+ python -m build --no-isolation
171
+ cd frontend
172
+ npx playwright install chromium
173
+ npm run test:e2e
174
+ ```
175
+
176
+ Target matrix: Django 5.2 / Python 3.11–3.14; Django 6.0 and 6.1 / Python
177
+ 3.12–3.14; PostgreSQL, MySQL/InnoDB, SQLite. CI exercises valid combinations and
178
+ the same transactional contract across databases. Consult CI results before
179
+ treating a combination as verified; local results are recorded in `docs/validation.md`.
180
+
181
+ ## Boundaries
182
+
183
+ Safe mode calls `full_clean()` and the instance's `save()` by default, including
184
+ normal model signals. Custom `ModelAdmin.save_model()`, custom Admin forms,
185
+ `save_related()` and external effects need an explicit adapter. Snapshot fingerprints
186
+ cover concrete model fields, not all intermediate history or related-table contents.
187
+
188
+ Sheets use the `default` database. User models require no extra fields or inheritance.
189
+ Existing scalar fields are editable; FK fields can be displayed/filtered but FK/M2M
190
+ editors, server undo, file import/export, fast-save, cursor pagination and persistent
191
+ formulas are deferred. Large operations require your own background adapter.
192
+
193
+ See [formulas](docs/formulas.md), [extension API](docs/extensions.md),
194
+ [background processing](docs/background.md), [HTTP API](docs/api.md), and
195
+ [release procedure](docs/releasing.md). Bundled dependency notices are in
196
+ `modelsheet/static/modelsheet/THIRD_PARTY_LICENSES.txt`.
@@ -0,0 +1,164 @@
1
+ # django-modelsheet
2
+
3
+ Spreadsheet editing for explicitly registered Django Admin models: cell/range editing,
4
+ local undo/redo, Decimal formulas, server-side filters, and previewed bulk changes.
5
+
6
+ **0.3.0a1 is an alpha.** The repository includes a working demo, compiled assets,
7
+ tests, and a compatibility CI matrix. A published PyPI release is not implied.
8
+
9
+ ## Quickstart
10
+
11
+ Install a built wheel (or `pip install .` from this repository). After publication:
12
+
13
+ ```sh
14
+ pip install django-modelsheet
15
+ ```
16
+
17
+ ```python
18
+ # settings.py
19
+ INSTALLED_APPS += ["modelsheet"]
20
+ MODELSHEET = {"models": ["shop.Product"], "page_size": 200}
21
+
22
+ # SQLite only: required for safe writes; PostgreSQL/MySQL use row locks.
23
+ DATABASES["default"].setdefault("OPTIONS", {})["transaction_mode"] = "IMMEDIATE"
24
+ ```
25
+
26
+ ```python
27
+ # admin.py
28
+ from django.contrib import admin
29
+ from modelsheet.admin import ModelSheetAdmin
30
+ from .models import Product
31
+
32
+ @admin.register(Product)
33
+ class ProductAdmin(ModelSheetAdmin):
34
+ list_display = ("name", "price", "stock")
35
+ search_fields = ("name",)
36
+ sheet_fields = ("name", "price", "stock")
37
+ sheet_editable = ("price", "stock")
38
+ sheet_computed_fields = {"inventory_value": "=[price]*[stock]"}
39
+ ```
40
+
41
+ ```sh
42
+ python manage.py migrate
43
+ python manage.py collectstatic --noinput
44
+ python manage.py modelsheet_roles # optional: creates groups, assigns no users
45
+ ```
46
+
47
+ Assign a group through Django Admin and mark the account as staff. Superusers can
48
+ use configured sheets immediately. Open **Spreadsheet** from the model list, or
49
+ visit `/admin/shop/product/sheet/`. No frontend build, CDN or extra URL include is
50
+ required in the consuming project. Admin's existing sessions and CSRF protection apply.
51
+
52
+ ## Using the sheet
53
+
54
+ - Double-click to edit; use the formula bar for a value or expression.
55
+ - Changes stay local until **Save**. Ctrl/Cmd+Z, Ctrl/Cmd+Y and Ctrl/Cmd+S operate on this buffer.
56
+ - Select a range, copy/paste TSV from Excel, or press Ctrl/Cmd+D to fill down.
57
+ - Readonly cells are skipped during paste with a notification. A save is atomic across all changed rows.
58
+ - Use **Sort** for ordered sort fields, or Alt+click a column header (Alt+Shift+click adds a field).
59
+ Ordinary header clicks select columns. Search and filters run on the server.
60
+ - **Bulk change** selects either highlighted page rows or every matching row, including other pages.
61
+ Build a preview, inspect its exact count/values, then confirm it. Editing inputs requires a new preview.
62
+ - **Ordinary Admin** is available only when its list can represent the same filter, ordering and page.
63
+
64
+ Numbers use a decimal point and dates use ISO 8601. Empty pasted cells become
65
+ NULL for nullable fields, otherwise empty text for text fields or a validation error.
66
+ Prefix text beginning with `=` with an apostrophe, for example `'=SUM(A1:A2)`.
67
+
68
+ ## Configuration
69
+
70
+ ```python
71
+ MODELSHEET = {
72
+ "models": ["shop.Product"],
73
+ "page_size": 200,
74
+ "max_page_size": 500,
75
+ "max_save_rows": 500,
76
+ "count_mode": "exact", # exact | none (no approximate COUNT promise)
77
+ "bulk_background_threshold": 5000,
78
+ "max_bulk_rows": 100000,
79
+ "chunk_size": 200,
80
+ "snapshot_ttl_seconds": 1800,
81
+ "preview_ttl_seconds": 1800,
82
+ "history_retention_days": 365,
83
+ "max_operations_per_minute": 30,
84
+ "max_request_bytes": 1000000,
85
+ "formula": {"max_length": 500, "max_depth": 10, "max_steps": 10000, "timeout_ms": 250},
86
+ "background_adapter": None,
87
+ }
88
+ ```
89
+
90
+ Run `python manage.py modelsheet_cleanup` periodically through your scheduler.
91
+ It removes expired snapshots and terminal/unused-preview operations beyond retention.
92
+ Do not reuse old idempotency keys after their operation has been purged.
93
+
94
+ ## Permissions
95
+
96
+ Access requires `is_active`, `is_staff`, `modelsheet.open`, the model's standard
97
+ `view` permission, and `ModelAdmin.has_view_permission()`. Editing additionally
98
+ requires the standard `change` permission and `has_change_permission()`.
99
+
100
+ Package permission codenames include app/model identifiers:
101
+
102
+ ```text
103
+ modelsheet.view_field_shop_product_price
104
+ modelsheet.edit_field_shop_product_price
105
+ modelsheet.bulk_shop_product
106
+ modelsheet.history_shop_product
107
+ ```
108
+
109
+ `get_queryset(request)` bounds every read/write. Object-level Admin hooks and the
110
+ configured policy apply in addition. Hidden fields cannot be used in filters,
111
+ sorting, formulas or history. Field permissions never grant model-level editing.
112
+
113
+ Group templates are opt-in and additive: Viewer, Editor, Operator, Auditor and Admin.
114
+ They grant access to the configured sheet fields; review these grants before
115
+ assigning users, especially sensitive columns. They do not grant superuser status.
116
+
117
+ ## Development and demo
118
+
119
+ ```sh
120
+ python -m pip install -e '.[test]'
121
+ cd frontend
122
+ npm ci
123
+ npm run build
124
+ npm test
125
+ cd ..
126
+ python manage.py migrate
127
+ python manage.py seed_demo --create-user
128
+ python manage.py runserver
129
+ ```
130
+
131
+ The explicit `--create-user` option creates a local-only `admin` / `admin` account.
132
+ The demo settings are not production settings. Open
133
+ `http://127.0.0.1:8000/admin/shop/product/sheet/`.
134
+
135
+ ```sh
136
+ python -m pytest
137
+ python -m ruff check .
138
+ python -m build --no-isolation
139
+ cd frontend
140
+ npx playwright install chromium
141
+ npm run test:e2e
142
+ ```
143
+
144
+ Target matrix: Django 5.2 / Python 3.11–3.14; Django 6.0 and 6.1 / Python
145
+ 3.12–3.14; PostgreSQL, MySQL/InnoDB, SQLite. CI exercises valid combinations and
146
+ the same transactional contract across databases. Consult CI results before
147
+ treating a combination as verified; local results are recorded in `docs/validation.md`.
148
+
149
+ ## Boundaries
150
+
151
+ Safe mode calls `full_clean()` and the instance's `save()` by default, including
152
+ normal model signals. Custom `ModelAdmin.save_model()`, custom Admin forms,
153
+ `save_related()` and external effects need an explicit adapter. Snapshot fingerprints
154
+ cover concrete model fields, not all intermediate history or related-table contents.
155
+
156
+ Sheets use the `default` database. User models require no extra fields or inheritance.
157
+ Existing scalar fields are editable; FK fields can be displayed/filtered but FK/M2M
158
+ editors, server undo, file import/export, fast-save, cursor pagination and persistent
159
+ formulas are deferred. Large operations require your own background adapter.
160
+
161
+ See [formulas](docs/formulas.md), [extension API](docs/extensions.md),
162
+ [background processing](docs/background.md), [HTTP API](docs/api.md), and
163
+ [release procedure](docs/releasing.md). Bundled dependency notices are in
164
+ `modelsheet/static/modelsheet/THIRD_PARTY_LICENSES.txt`.
@@ -0,0 +1,15 @@
1
+ # Security
2
+
3
+ For a suspected vulnerability, use the repository's private vulnerability reporting
4
+ feature when available; do not disclose exploit details or customer data in a public
5
+ issue. If private reporting is unavailable, ask the maintainer for a private channel
6
+ without including sensitive details.
7
+
8
+ The package is currently an unreleased alpha. Track Django security updates and pin
9
+ supported patch versions in deployments. Follow least privilege when assigning group
10
+ templates. Audit history stores plaintext before/after values in the application DB;
11
+ protect database access/backups and choose an appropriate retention period.
12
+
13
+ Formula expressions have no host-language execution facilities. UI values are rendered
14
+ as text. Mutations use CSRF and server-side field/row checks. External application
15
+ hooks and background context restoration are part of the integrating project's trust boundary.
File without changes
@@ -0,0 +1,76 @@
1
+ """Development/test project. Never deploy with this secret or DEBUG enabled."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ BASE_DIR = Path(__file__).resolve().parent.parent
7
+ SECRET_KEY = "modelsheet-local-demo-only-do-not-deploy"
8
+ DEBUG = True
9
+ ALLOWED_HOSTS = ["localhost", "127.0.0.1", "testserver"]
10
+ INSTALLED_APPS = [
11
+ "django.contrib.admin",
12
+ "django.contrib.auth",
13
+ "django.contrib.contenttypes",
14
+ "django.contrib.sessions",
15
+ "django.contrib.messages",
16
+ "django.contrib.staticfiles",
17
+ "modelsheet",
18
+ "demo.shop",
19
+ ]
20
+ MIDDLEWARE = [
21
+ "django.middleware.security.SecurityMiddleware",
22
+ "django.contrib.sessions.middleware.SessionMiddleware",
23
+ "django.middleware.locale.LocaleMiddleware",
24
+ "django.middleware.common.CommonMiddleware",
25
+ "django.middleware.csrf.CsrfViewMiddleware",
26
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
27
+ "django.contrib.messages.middleware.MessageMiddleware",
28
+ ]
29
+ ROOT_URLCONF = "demo.urls"
30
+ TEMPLATES = [
31
+ {
32
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
33
+ "APP_DIRS": True,
34
+ "OPTIONS": {
35
+ "context_processors": [
36
+ "django.template.context_processors.request",
37
+ "django.contrib.auth.context_processors.auth",
38
+ "django.contrib.messages.context_processors.messages",
39
+ ]
40
+ },
41
+ }
42
+ ]
43
+ DATABASES = {
44
+ "default": {
45
+ "ENGINE": "django.db.backends.sqlite3",
46
+ "NAME": BASE_DIR / "demo.sqlite3",
47
+ "OPTIONS": {"transaction_mode": "IMMEDIATE", "timeout": 20},
48
+ }
49
+ }
50
+ if os.environ.get("MODELSHEET_DB") == "postgres":
51
+ DATABASES["default"] = {
52
+ "ENGINE": "django.db.backends.postgresql",
53
+ "NAME": "modelsheet",
54
+ "USER": "modelsheet",
55
+ "PASSWORD": "modelsheet",
56
+ "HOST": "127.0.0.1",
57
+ "PORT": "5432",
58
+ }
59
+ elif os.environ.get("MODELSHEET_DB") == "mysql":
60
+ DATABASES["default"] = {
61
+ "ENGINE": "django.db.backends.mysql",
62
+ "NAME": "modelsheet",
63
+ "USER": "root",
64
+ "PASSWORD": "modelsheet",
65
+ "HOST": "127.0.0.1",
66
+ "PORT": "3306",
67
+ "OPTIONS": {"charset": "utf8mb4", "init_command": "SET default_storage_engine=INNODB"},
68
+ }
69
+ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
70
+ STATIC_URL = "/static/"
71
+ STATIC_ROOT = BASE_DIR / "staticfiles"
72
+ LANGUAGE_CODE = "en"
73
+ LANGUAGES = [("en", "English"), ("ru", "Русский")]
74
+ TIME_ZONE = "UTC"
75
+ USE_TZ = True
76
+ MODELSHEET = {"models": ["shop.Product", "shop.Order"], "page_size": 200}
File without changes
@@ -0,0 +1,46 @@
1
+ from django.contrib import admin
2
+
3
+ from modelsheet.admin import ModelSheetAdmin
4
+ from .models import Category, Order, Product
5
+
6
+
7
+ @admin.register(Product)
8
+ class ProductAdmin(ModelSheetAdmin):
9
+ list_display = ("name", "price", "stock", "status", "category", "active")
10
+ list_filter = ("status", "category", "active")
11
+ search_fields = ("name", "note")
12
+ sheet_fields = (
13
+ "name",
14
+ "price",
15
+ "cost_price",
16
+ "stock",
17
+ "status",
18
+ "category",
19
+ "active",
20
+ "available_on",
21
+ "note",
22
+ )
23
+ sheet_editable = (
24
+ "name",
25
+ "price",
26
+ "cost_price",
27
+ "stock",
28
+ "status",
29
+ "active",
30
+ "available_on",
31
+ "note",
32
+ )
33
+ sheet_computed_fields = {"inventory_value": "=[price]*[stock]"}
34
+ list_per_page = 200
35
+
36
+
37
+ @admin.register(Order)
38
+ class OrderAdmin(ModelSheetAdmin):
39
+ list_display = ("reference", "total", "paid")
40
+ sheet_fields = ("reference", "total", "paid", "created_at")
41
+ sheet_editable = ("paid",)
42
+ search_fields = ("reference",)
43
+ list_per_page = 200
44
+
45
+
46
+ admin.site.register(Category)
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class ShopConfig(AppConfig):
5
+ name = "demo.shop"
6
+ label = "shop"
@@ -0,0 +1,38 @@
1
+ from decimal import Decimal
2
+
3
+ from django.contrib.auth import get_user_model
4
+ from django.core.management.base import BaseCommand
5
+
6
+ from demo.shop.models import Category, Product
7
+
8
+
9
+ class Command(BaseCommand):
10
+ help = "Seed local demo products; creates admin/admin only when --create-user is supplied."
11
+
12
+ def add_arguments(self, parser):
13
+ parser.add_argument("--rows", type=int, default=500)
14
+ parser.add_argument("--create-user", action="store_true")
15
+
16
+ def handle(self, *args, **options):
17
+ category, _ = Category.objects.get_or_create(name="Consumables")
18
+ Product.objects.bulk_create(
19
+ [
20
+ Product(
21
+ name=f"Product {i:07}",
22
+ price=Decimal("10.00") + i,
23
+ stock=i % 30,
24
+ category=category,
25
+ )
26
+ for i in range(options["rows"])
27
+ ],
28
+ ignore_conflicts=True,
29
+ )
30
+ if options["create_user"]:
31
+ user, created = get_user_model().objects.get_or_create(
32
+ username="admin", defaults={"is_staff": True, "is_superuser": True}
33
+ )
34
+ if created:
35
+ user.set_password("admin")
36
+ user.save()
37
+ self.stdout.write("Local demo login: admin / admin")
38
+ self.stdout.write(f"Products: {Product.objects.count()}")