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.
- django_modelsheet-0.3.0a1/CHANGELOG.md +13 -0
- django_modelsheet-0.3.0a1/CONTRIBUTING.md +18 -0
- django_modelsheet-0.3.0a1/LICENSE +21 -0
- django_modelsheet-0.3.0a1/MANIFEST.in +13 -0
- django_modelsheet-0.3.0a1/PKG-INFO +196 -0
- django_modelsheet-0.3.0a1/README.md +164 -0
- django_modelsheet-0.3.0a1/SECURITY.md +15 -0
- django_modelsheet-0.3.0a1/demo/__init__.py +0 -0
- django_modelsheet-0.3.0a1/demo/settings.py +76 -0
- django_modelsheet-0.3.0a1/demo/shop/__init__.py +0 -0
- django_modelsheet-0.3.0a1/demo/shop/admin.py +46 -0
- django_modelsheet-0.3.0a1/demo/shop/apps.py +6 -0
- django_modelsheet-0.3.0a1/demo/shop/management/__init__.py +0 -0
- django_modelsheet-0.3.0a1/demo/shop/management/commands/__init__.py +0 -0
- django_modelsheet-0.3.0a1/demo/shop/management/commands/seed_demo.py +38 -0
- django_modelsheet-0.3.0a1/demo/shop/migrations/0001_initial.py +75 -0
- django_modelsheet-0.3.0a1/demo/shop/migrations/__init__.py +0 -0
- django_modelsheet-0.3.0a1/demo/shop/models.py +39 -0
- django_modelsheet-0.3.0a1/demo/urls.py +4 -0
- django_modelsheet-0.3.0a1/django_modelsheet.egg-info/PKG-INFO +196 -0
- django_modelsheet-0.3.0a1/django_modelsheet.egg-info/SOURCES.txt +94 -0
- django_modelsheet-0.3.0a1/django_modelsheet.egg-info/dependency_links.txt +1 -0
- django_modelsheet-0.3.0a1/django_modelsheet.egg-info/requires.txt +15 -0
- django_modelsheet-0.3.0a1/django_modelsheet.egg-info/top_level.txt +1 -0
- django_modelsheet-0.3.0a1/docs/api.md +52 -0
- django_modelsheet-0.3.0a1/docs/background.md +52 -0
- django_modelsheet-0.3.0a1/docs/benchmark-100k.json +36 -0
- django_modelsheet-0.3.0a1/docs/benchmark-1m.json +36 -0
- django_modelsheet-0.3.0a1/docs/benchmarks.md +17 -0
- django_modelsheet-0.3.0a1/docs/extensions.md +52 -0
- django_modelsheet-0.3.0a1/docs/formulas.md +46 -0
- django_modelsheet-0.3.0a1/docs/releasing.md +57 -0
- django_modelsheet-0.3.0a1/docs/validation.md +76 -0
- django_modelsheet-0.3.0a1/frontend/e2e/sheet.spec.ts +167 -0
- django_modelsheet-0.3.0a1/frontend/package-lock.json +1707 -0
- django_modelsheet-0.3.0a1/frontend/package.json +23 -0
- django_modelsheet-0.3.0a1/frontend/playwright.config.ts +13 -0
- django_modelsheet-0.3.0a1/frontend/src/buffer.ts +102 -0
- django_modelsheet-0.3.0a1/frontend/src/formula.ts +260 -0
- django_modelsheet-0.3.0a1/frontend/src/i18n.ts +24 -0
- django_modelsheet-0.3.0a1/frontend/src/main.ts +495 -0
- django_modelsheet-0.3.0a1/frontend/src/style.css +9 -0
- django_modelsheet-0.3.0a1/frontend/tests/buffer.test.ts +43 -0
- django_modelsheet-0.3.0a1/frontend/tests/formula.test.ts +22 -0
- django_modelsheet-0.3.0a1/frontend/tsconfig.json +1 -0
- django_modelsheet-0.3.0a1/frontend/vite.config.ts +12 -0
- django_modelsheet-0.3.0a1/frontend/vitest.config.ts +3 -0
- django_modelsheet-0.3.0a1/manage.py +8 -0
- django_modelsheet-0.3.0a1/modelsheet/__init__.py +1 -0
- django_modelsheet-0.3.0a1/modelsheet/adapters.py +121 -0
- django_modelsheet-0.3.0a1/modelsheet/admin.py +325 -0
- django_modelsheet-0.3.0a1/modelsheet/apps.py +14 -0
- django_modelsheet-0.3.0a1/modelsheet/background.py +44 -0
- django_modelsheet-0.3.0a1/modelsheet/bulk.py +410 -0
- django_modelsheet-0.3.0a1/modelsheet/checks.py +90 -0
- django_modelsheet-0.3.0a1/modelsheet/conf.py +42 -0
- django_modelsheet-0.3.0a1/modelsheet/errors.py +18 -0
- django_modelsheet-0.3.0a1/modelsheet/formula/__init__.py +3 -0
- django_modelsheet-0.3.0a1/modelsheet/formula/engine.py +392 -0
- django_modelsheet-0.3.0a1/modelsheet/locale/ru/LC_MESSAGES/django.mo +0 -0
- django_modelsheet-0.3.0a1/modelsheet/locale/ru/LC_MESSAGES/django.po +8 -0
- django_modelsheet-0.3.0a1/modelsheet/locale/ru/LC_MESSAGES/djangojs.mo +0 -0
- django_modelsheet-0.3.0a1/modelsheet/locale/ru/LC_MESSAGES/djangojs.po +188 -0
- django_modelsheet-0.3.0a1/modelsheet/management/__init__.py +0 -0
- django_modelsheet-0.3.0a1/modelsheet/management/commands/__init__.py +0 -0
- django_modelsheet-0.3.0a1/modelsheet/management/commands/modelsheet_cleanup.py +24 -0
- django_modelsheet-0.3.0a1/modelsheet/management/commands/modelsheet_roles.py +47 -0
- django_modelsheet-0.3.0a1/modelsheet/metadata.py +42 -0
- django_modelsheet-0.3.0a1/modelsheet/migrations/0001_initial.py +114 -0
- django_modelsheet-0.3.0a1/modelsheet/migrations/__init__.py +0 -0
- django_modelsheet-0.3.0a1/modelsheet/models.py +63 -0
- django_modelsheet-0.3.0a1/modelsheet/permissions.py +72 -0
- django_modelsheet-0.3.0a1/modelsheet/py.typed +0 -0
- django_modelsheet-0.3.0a1/modelsheet/query.py +133 -0
- django_modelsheet-0.3.0a1/modelsheet/services.py +490 -0
- django_modelsheet-0.3.0a1/modelsheet/static/modelsheet/THIRD_PARTY_LICENSES.txt +49 -0
- django_modelsheet-0.3.0a1/modelsheet/static/modelsheet/sheet.css +1 -0
- django_modelsheet-0.3.0a1/modelsheet/static/modelsheet/sheet.js +13482 -0
- django_modelsheet-0.3.0a1/modelsheet/templates/modelsheet/audit.html +14 -0
- django_modelsheet-0.3.0a1/modelsheet/templates/modelsheet/change_list.html +6 -0
- django_modelsheet-0.3.0a1/modelsheet/templates/modelsheet/sheet.html +11 -0
- django_modelsheet-0.3.0a1/modelsheet/templates/modelsheet/unavailable.html +2 -0
- django_modelsheet-0.3.0a1/modelsheet/views.py +91 -0
- django_modelsheet-0.3.0a1/pyproject.toml +49 -0
- django_modelsheet-0.3.0a1/scripts/benchmark.py +116 -0
- django_modelsheet-0.3.0a1/scripts/check_pypi_name.py +25 -0
- django_modelsheet-0.3.0a1/scripts/package_smoke.py +54 -0
- django_modelsheet-0.3.0a1/setup.cfg +4 -0
- django_modelsheet-0.3.0a1/tests/__init__.py +0 -0
- django_modelsheet-0.3.0a1/tests/conftest.py +65 -0
- django_modelsheet-0.3.0a1/tests/formula_cases.json +51 -0
- django_modelsheet-0.3.0a1/tests/test_api.py +311 -0
- django_modelsheet-0.3.0a1/tests/test_bulk.py +209 -0
- django_modelsheet-0.3.0a1/tests/test_concurrency.py +62 -0
- django_modelsheet-0.3.0a1/tests/test_configuration.py +102 -0
- 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)
|
|
File without changes
|
|
File without changes
|
|
@@ -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()}")
|