django-modelsheet 0.3.0__py3-none-any.whl

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 (42) hide show
  1. django_modelsheet-0.3.0.dist-info/METADATA +210 -0
  2. django_modelsheet-0.3.0.dist-info/RECORD +42 -0
  3. django_modelsheet-0.3.0.dist-info/WHEEL +5 -0
  4. django_modelsheet-0.3.0.dist-info/licenses/LICENSE +21 -0
  5. django_modelsheet-0.3.0.dist-info/top_level.txt +1 -0
  6. modelsheet/__init__.py +1 -0
  7. modelsheet/adapters.py +121 -0
  8. modelsheet/admin.py +325 -0
  9. modelsheet/apps.py +14 -0
  10. modelsheet/background.py +44 -0
  11. modelsheet/bulk.py +430 -0
  12. modelsheet/checks.py +90 -0
  13. modelsheet/conf.py +42 -0
  14. modelsheet/errors.py +18 -0
  15. modelsheet/formula/__init__.py +3 -0
  16. modelsheet/formula/engine.py +422 -0
  17. modelsheet/locale/ru/LC_MESSAGES/django.mo +0 -0
  18. modelsheet/locale/ru/LC_MESSAGES/django.po +8 -0
  19. modelsheet/locale/ru/LC_MESSAGES/djangojs.mo +0 -0
  20. modelsheet/locale/ru/LC_MESSAGES/djangojs.po +188 -0
  21. modelsheet/management/__init__.py +0 -0
  22. modelsheet/management/commands/__init__.py +0 -0
  23. modelsheet/management/commands/modelsheet_cleanup.py +24 -0
  24. modelsheet/management/commands/modelsheet_roles.py +58 -0
  25. modelsheet/metadata.py +42 -0
  26. modelsheet/migrations/0001_initial.py +114 -0
  27. modelsheet/migrations/0002_operation_preview_expires_at.py +26 -0
  28. modelsheet/migrations/__init__.py +0 -0
  29. modelsheet/models.py +64 -0
  30. modelsheet/permissions.py +72 -0
  31. modelsheet/py.typed +0 -0
  32. modelsheet/query.py +133 -0
  33. modelsheet/registry.py +22 -0
  34. modelsheet/services.py +491 -0
  35. modelsheet/static/modelsheet/THIRD_PARTY_LICENSES.txt +49 -0
  36. modelsheet/static/modelsheet/sheet.css +1 -0
  37. modelsheet/static/modelsheet/sheet.js +13538 -0
  38. modelsheet/templates/modelsheet/audit.html +14 -0
  39. modelsheet/templates/modelsheet/change_list.html +6 -0
  40. modelsheet/templates/modelsheet/sheet.html +11 -0
  41. modelsheet/templates/modelsheet/unavailable.html +2 -0
  42. modelsheet/views.py +91 -0
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-modelsheet
3
+ Version: 0.3.0
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://svalench.github.io/django-modelsheet/
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 :: 5 - Production/Stable
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
+ Requires-Dist: PyYAML>=6; extra == "test"
28
+ Provides-Extra: postgres
29
+ Requires-Dist: psycopg[binary]>=3.2; extra == "postgres"
30
+ Provides-Extra: mysql
31
+ Requires-Dist: mysqlclient>=2.2; extra == "mysql"
32
+ Dynamic: license-file
33
+
34
+ # django-modelsheet
35
+
36
+ Spreadsheet editing for explicitly registered Django Admin models: cell/range editing,
37
+ local undo/redo, Decimal formulas, server-side filters, and previewed bulk changes.
38
+
39
+ **0.3.0 is the first stable release.** [Release notes](https://github.com/svalench/django-modelsheet/releases/tag/v0.3.0) · [PyPI](https://pypi.org/project/django-modelsheet/0.3.0/)
40
+ The repository includes a working demo, compiled assets, tests, and a compatibility CI matrix.
41
+
42
+ **[Documentation website →](https://svalench.github.io/django-modelsheet/)** —
43
+ installation, examples, formulas, bulk operations and extension APIs in English, Simplified Chinese and Belarusian.
44
+
45
+ ## Quickstart
46
+
47
+ Install the stable release:
48
+
49
+ ```sh
50
+ pip install django-modelsheet==0.3.0
51
+ ```
52
+
53
+ ```python
54
+ # settings.py
55
+ INSTALLED_APPS += ["modelsheet"]
56
+
57
+ # SQLite only: required for safe writes; PostgreSQL/MySQL use row locks.
58
+ DATABASES["default"].setdefault("OPTIONS", {})["transaction_mode"] = "IMMEDIATE"
59
+ ```
60
+
61
+ ```python
62
+ # admin.py
63
+ from django.contrib import admin
64
+ from modelsheet.admin import ModelSheetAdmin
65
+ from .models import Product
66
+
67
+ @admin.register(Product)
68
+ class ProductAdmin(ModelSheetAdmin):
69
+ list_display = ("name", "price", "stock")
70
+ search_fields = ("name",)
71
+ sheet_fields = ("name", "price", "stock")
72
+ sheet_editable = ("price", "stock")
73
+ sheet_computed_fields = {"inventory_value": "=[price]*[stock]"}
74
+ ```
75
+
76
+ ```sh
77
+ python manage.py migrate
78
+ python manage.py collectstatic --noinput
79
+ python manage.py modelsheet_roles # optional: creates groups, assigns no users
80
+ ```
81
+
82
+ Registering `ModelSheetAdmin` enables the sheet; no model list in settings is needed.
83
+
84
+ Assign a group through Django Admin and mark the account as staff. Superusers can
85
+ use configured sheets immediately. Open **Spreadsheet** from the model list, or
86
+ visit `/admin/shop/product/sheet/`. No frontend build, CDN or extra URL include is
87
+ required in the consuming project. Admin's existing sessions and CSRF protection apply.
88
+
89
+ ## Upgrading to 0.3.0
90
+
91
+ Back up the database and pause writes/background workers. Install the new package,
92
+ run `python manage.py migrate` and `python manage.py collectstatic --noinput`, then
93
+ restart the application/workers and reload browser tabs. Migration `0002` preserves
94
+ history and operation items, but expires legacy unconfirmed previews: build a new
95
+ preview before confirming. Already confirmed runs retain their resume behavior.
96
+ New preview deadlines start when preparation completes and cannot be extended by
97
+ cancel/resume or settings changes. Pin `django-modelsheet==0.3.0`.
98
+
99
+ ## Using the sheet
100
+
101
+ - Double-click to edit; use the formula bar for a value or expression.
102
+ - Changes stay local until **Save**. Ctrl/Cmd+Z, Ctrl/Cmd+Y and Ctrl/Cmd+S operate on this buffer.
103
+ - Select a range, copy/paste TSV from Excel, or press Ctrl/Cmd+D to fill down.
104
+ - Readonly cells are skipped during paste with a notification. A save is atomic across all changed rows.
105
+ - Use **Sort** for ordered sort fields, or Alt+click a column header (Alt+Shift+click adds a field).
106
+ Ordinary header clicks select columns. Search and filters run on the server.
107
+ - **Bulk change** selects either highlighted page rows or every matching row, including other pages.
108
+ Build a preview, inspect its exact count/values, then confirm it. Editing inputs requires a new preview.
109
+ - **Ordinary Admin** is available only when its list can represent the same filter, ordering and page.
110
+
111
+ Numbers use a decimal point and dates use ISO 8601. Empty pasted cells become
112
+ NULL for nullable fields, otherwise empty text for text fields or a validation error.
113
+ Prefix text beginning with `=` with an apostrophe, for example `'=SUM(A1:A2)`.
114
+
115
+ ## Configuration
116
+
117
+ ```python
118
+ MODELSHEET = {
119
+ "page_size": 200,
120
+ "max_page_size": 500,
121
+ "max_save_rows": 500,
122
+ "count_mode": "exact", # exact | none (no approximate COUNT promise)
123
+ "bulk_background_threshold": 5000,
124
+ "max_bulk_rows": 100000,
125
+ "chunk_size": 200,
126
+ "snapshot_ttl_seconds": 1800,
127
+ "preview_ttl_seconds": 1800,
128
+ "history_retention_days": 365,
129
+ "max_operations_per_minute": 30,
130
+ "max_request_bytes": 1000000,
131
+ "formula": {"max_length": 500, "max_depth": 10, "max_steps": 10000, "timeout_ms": 250},
132
+ "background_adapter": None,
133
+ }
134
+ ```
135
+
136
+ Run `python manage.py modelsheet_cleanup` periodically through your scheduler.
137
+ It removes expired snapshots and terminal/unused-preview operations beyond retention.
138
+ Do not reuse old idempotency keys after their operation has been purged.
139
+
140
+ ## Permissions
141
+
142
+ Access requires `is_active`, `is_staff`, `modelsheet.open`, the model's standard
143
+ `view` permission, and `ModelAdmin.has_view_permission()`. Editing additionally
144
+ requires the standard `change` permission and `has_change_permission()`.
145
+
146
+ Package permission codenames include app/model identifiers:
147
+
148
+ ```text
149
+ modelsheet.view_field_shop_product_price
150
+ modelsheet.edit_field_shop_product_price
151
+ modelsheet.bulk_shop_product
152
+ modelsheet.history_shop_product
153
+ ```
154
+
155
+ `get_queryset(request)` bounds every read/write. Object-level Admin hooks and the
156
+ configured policy apply in addition. Hidden fields cannot be used in filters,
157
+ sorting, formulas or history. Field permissions never grant model-level editing.
158
+
159
+ Group templates are opt-in and additive: Viewer, Editor, Operator, Auditor and Admin.
160
+ They grant access to the configured sheet fields; review these grants before
161
+ assigning users, especially sensitive columns. They do not grant superuser status.
162
+
163
+ ## Development and demo
164
+
165
+ ```sh
166
+ python -m pip install -e '.[test]'
167
+ cd frontend
168
+ npm ci
169
+ npm run build
170
+ npm test
171
+ cd ..
172
+ python manage.py migrate
173
+ python manage.py seed_demo --create-user
174
+ python manage.py runserver
175
+ ```
176
+
177
+ The explicit `--create-user` option creates a local-only `admin` / `admin` account.
178
+ The demo settings are not production settings. Open
179
+ `http://127.0.0.1:8000/admin/shop/product/sheet/`.
180
+
181
+ ```sh
182
+ python -m pytest
183
+ python -m ruff check .
184
+ python -m build --no-isolation
185
+ cd frontend
186
+ npx playwright install chromium
187
+ npm run test:e2e
188
+ ```
189
+
190
+ Target matrix: Django 5.2 / Python 3.11–3.14; Django 6.0 and 6.1 / Python
191
+ 3.12–3.14; PostgreSQL, MySQL/InnoDB, SQLite. CI exercises valid combinations and
192
+ the same transactional contract across databases. Consult CI results before
193
+ treating a combination as verified; local results are recorded in `docs/validation.md`.
194
+
195
+ ## Boundaries
196
+
197
+ Safe mode calls `full_clean()` and the instance's `save()` by default, including
198
+ normal model signals. Custom `ModelAdmin.save_model()`, custom Admin forms,
199
+ `save_related()` and external effects need an explicit adapter. Snapshot fingerprints
200
+ cover concrete model fields, not all intermediate history or related-table contents.
201
+
202
+ Sheets use the `default` database. User models require no extra fields or inheritance.
203
+ Existing scalar fields are editable; FK fields can be displayed/filtered but FK/M2M
204
+ editors, server undo, file import/export, fast-save, cursor pagination and persistent
205
+ formulas are deferred. Large operations require your own background adapter.
206
+
207
+ See [formulas](docs/formulas.md), [extension API](docs/extensions.md),
208
+ [background processing](docs/background.md), [HTTP API](docs/api.md), and
209
+ [release procedure](docs/releasing.md). Bundled dependency notices are in
210
+ `modelsheet/static/modelsheet/THIRD_PARTY_LICENSES.txt`.
@@ -0,0 +1,42 @@
1
+ django_modelsheet-0.3.0.dist-info/licenses/LICENSE,sha256=MmQND-_DSLg3oiVoo63Z3JCB2OXKqZRSXroN0t1Ai7I,1087
2
+ modelsheet/__init__.py,sha256=VrXpHDu3erkzwl_WXrqINBm9xWkcyUy53IQOj042dOs,22
3
+ modelsheet/adapters.py,sha256=k--zM4F04BMAlEUdmnrRh-JGB8bDtxZsTXQ-4YnCNZQ,4457
4
+ modelsheet/admin.py,sha256=WLuzbvFxSDca7i4BMbESKEqgX4CvXmG9_JTWa0vdTXo,13138
5
+ modelsheet/apps.py,sha256=ddPeL-HoSEOP2VVyrrbao4h97dAgKlqM6hluy-ozJH0,455
6
+ modelsheet/background.py,sha256=VBLNlkcnrNiel6LBwTLkMjOAlPwsPxBG7F3YRKLs28c,1919
7
+ modelsheet/bulk.py,sha256=1ZmbIGwpTogu6FVF-Rkp7nvWrwPIP-0rEtU4X31YLuY,18149
8
+ modelsheet/checks.py,sha256=xVXGnuY6rJMTvlh22mlWdcG1Kc1IBwsnP8SbEVF4iAY,3989
9
+ modelsheet/conf.py,sha256=pjoPc5kI-Rc9lM9b6T6k1NI6XUYABDuOFLH3Ew_tFRQ,1378
10
+ modelsheet/errors.py,sha256=fs0ucDObHYetMqV6olQ3miff0VqOU0Vx0KgzV7Epqmc,614
11
+ modelsheet/metadata.py,sha256=ApFru-G0EfxQoZ6HcK4g2rAb7fcfuvpdyAzW6O0TY40,1405
12
+ modelsheet/models.py,sha256=CoHciVpShLTDhfgMXpKLVlYTrDJoWJFZDQjf2iilNsw,2604
13
+ modelsheet/permissions.py,sha256=9zniYFREwDVg85MN3mOMhVjL2x2DjA68PbzWV2XgcTg,2841
14
+ modelsheet/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ modelsheet/query.py,sha256=dYHKrNmlPhqZEy_liR1bU4pCYozc_PpZ-HPwkBbxN-A,5246
16
+ modelsheet/registry.py,sha256=uACRZGuDCDHLCk-IByfqG5rS7jTZL-aQH6CPHoujYcY,605
17
+ modelsheet/services.py,sha256=_5w_ELlPqAwwhZnb6JKozxtOawnxpgHr7X6qao8xZ_8,18683
18
+ modelsheet/views.py,sha256=mPgpWX_4x77tMetOuWGL9W1Plirxp7yV5ihmHBfSm_A,3680
19
+ modelsheet/formula/__init__.py,sha256=QyDX-f5Dvqmnovz9zUC-On05OWlIp5I_xDK3qnkG1WY,125
20
+ modelsheet/formula/engine.py,sha256=ec_v5156CrpQPOzmv5fgQozR__LjXw88GXams80BaiU,14084
21
+ modelsheet/locale/ru/LC_MESSAGES/django.mo,sha256=CAVfcPAzm7SzCfRXErTD-5xbHlO7lfHh3OOLpbH0CZA,259
22
+ modelsheet/locale/ru/LC_MESSAGES/django.po,sha256=_jEJe0eYK6iuF6MzjtJXFnGMaUY1hUaq3Jw4Bfz7Hhg,246
23
+ modelsheet/locale/ru/LC_MESSAGES/djangojs.mo,sha256=EzdptKmDcsPL68r_NMFECeZqOZ-5uEyBfdR3aNw4r4Q,3431
24
+ modelsheet/locale/ru/LC_MESSAGES/djangojs.po,sha256=SoxFKh-_JOYM2elhdxMqpG7J6C4t_bWtUpeQQ25pWWQ,3536
25
+ modelsheet/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ modelsheet/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ modelsheet/management/commands/modelsheet_cleanup.py,sha256=pwJ-PFU4NBEEp7srW4h8bZPQ9R6WiPDk3S_TWi85ttY,905
28
+ modelsheet/management/commands/modelsheet_roles.py,sha256=MsGz87KORtKOt5ihnXcBRc77roWJneyWhN5qjP0wuo8,2624
29
+ modelsheet/migrations/0001_initial.py,sha256=uP5UZTy3U80YhE5kfft1kOnObt8qlVk94-WFDP8q5zY,4589
30
+ modelsheet/migrations/0002_operation_preview_expires_at.py,sha256=zqZ0dW3Z4XCMFUNM7c-bCMSqRieIoU9NtA4Wh1QgYP0,1117
31
+ modelsheet/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
+ modelsheet/static/modelsheet/THIRD_PARTY_LICENSES.txt,sha256=0opprOP9KVjqzCKxO-Uz5x_wleW0c4CkJS5sBi7HP2Y,2188
33
+ modelsheet/static/modelsheet/sheet.css,sha256=-oCXbWclpUbFXEIevimlCzWZcvMsUb1N7bgQaBy9gU4,33181
34
+ modelsheet/static/modelsheet/sheet.js,sha256=zKuQay5gjSaBCmaCgIGL_r-qRRh-aCylUW406D2sPNQ,673053
35
+ modelsheet/templates/modelsheet/audit.html,sha256=BbTLwkuAccbglySQWlcwVmsFOSPrWm-AEfH7zXLql5M,753
36
+ modelsheet/templates/modelsheet/change_list.html,sha256=LZh_Jzqi2N0FO9jLoMdu-OlEJAIcHjsvt-seRjnHY8A,231
37
+ modelsheet/templates/modelsheet/sheet.html,sha256=jkpWDt_YdYfIBKI4qYC75v6X6TrVzCj_WTOQnLRCzkQ,531
38
+ modelsheet/templates/modelsheet/unavailable.html,sha256=1Nihf4i4RAobf6qK-zg1AG0fauf7Gp3Xbfr0i5WUXyc,129
39
+ django_modelsheet-0.3.0.dist-info/METADATA,sha256=gyFPXNSzAUJ27wcoFYIkgRNMB9_2m6qBj6t0szlK-Ak,8638
40
+ django_modelsheet-0.3.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
41
+ django_modelsheet-0.3.0.dist-info/top_level.txt,sha256=bUovMtlnvML9O2UB-RuPHhcidwi_vXCkTlv9V727lak,11
42
+ django_modelsheet-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ modelsheet
modelsheet/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
modelsheet/adapters.py ADDED
@@ -0,0 +1,121 @@
1
+ import datetime as dt
2
+ from decimal import Decimal, ROUND_HALF_EVEN, localcontext
3
+
4
+ from django.core.exceptions import ValidationError
5
+ from django.conf import settings
6
+ from django.db import models
7
+ from django.utils import timezone
8
+
9
+
10
+ def serialize(value):
11
+ if isinstance(value, Decimal):
12
+ return str(value)
13
+ if isinstance(value, (dt.datetime, dt.date, dt.time)):
14
+ return value.isoformat()
15
+ if isinstance(value, (str, int, float, bool)) or value is None:
16
+ return value
17
+ return str(value)
18
+
19
+
20
+ class FieldAdapter:
21
+ def supported(self, field):
22
+ return (
23
+ isinstance(
24
+ field,
25
+ (
26
+ models.CharField,
27
+ models.TextField,
28
+ models.IntegerField,
29
+ models.DecimalField,
30
+ models.FloatField,
31
+ models.BooleanField,
32
+ models.DateField,
33
+ models.TimeField,
34
+ models.UUIDField,
35
+ models.ForeignKey,
36
+ ),
37
+ )
38
+ and not field.primary_key
39
+ )
40
+
41
+ def editable(self, field):
42
+ return self.supported(field) and field.editable and not isinstance(field, models.ForeignKey)
43
+
44
+ def serialize(self, field, instance):
45
+ return serialize(field.value_from_object(instance))
46
+
47
+ def parse(self, field, value):
48
+ if isinstance(value, (dict, list)):
49
+ raise ValidationError("A scalar value is required.")
50
+ if value is None or value == "":
51
+ if field.null:
52
+ return None
53
+ if isinstance(field, (models.CharField, models.TextField)):
54
+ return ""
55
+ raise ValidationError("This field cannot be empty.")
56
+ if isinstance(field, models.BooleanField) and isinstance(value, str):
57
+ if value.lower() in ("true", "1"):
58
+ value = True
59
+ elif value.lower() in ("false", "0"):
60
+ value = False
61
+ if isinstance(field, models.DecimalField):
62
+ value = Decimal(str(value))
63
+ if not value.is_finite():
64
+ raise ValidationError("A finite decimal is required.")
65
+ with localcontext() as context:
66
+ context.prec = max(28, field.max_digits + 2)
67
+ value = value.quantize(
68
+ Decimal(1).scaleb(-field.decimal_places), rounding=ROUND_HALF_EVEN
69
+ )
70
+ if isinstance(field, models.IntegerField) and not isinstance(field, models.BooleanField):
71
+ integer = Decimal(str(value))
72
+ if (
73
+ isinstance(value, bool)
74
+ or not integer.is_finite()
75
+ or integer.adjusted() > 19
76
+ or integer != integer.to_integral_value()
77
+ ):
78
+ raise ValidationError("An integer is required.")
79
+ if isinstance(field, models.FloatField):
80
+ import math
81
+
82
+ if not math.isfinite(float(value)):
83
+ raise ValidationError("A finite number is required.")
84
+ value = field.to_python(value)
85
+ if isinstance(value, dt.datetime) and timezone.is_naive(value) and settings.USE_TZ:
86
+ value = timezone.make_aware(value, dt.timezone.utc)
87
+ elif isinstance(value, dt.datetime) and timezone.is_aware(value) and not settings.USE_TZ:
88
+ value = value.astimezone(dt.timezone.utc).replace(tzinfo=None)
89
+ return value
90
+
91
+ def metadata(self, field):
92
+ return {
93
+ "name": field.name,
94
+ "label": str(field.verbose_name),
95
+ "type": field.get_internal_type(),
96
+ "nullable": field.null,
97
+ "choices": [[serialize(k), str(v)] for k, v in field.flatchoices],
98
+ }
99
+
100
+
101
+ class SaveAdapter:
102
+ """Override for project validation/business logic. External effects must be idempotent."""
103
+
104
+ def validate(self, instance, request):
105
+ instance.full_clean()
106
+
107
+ def save(self, instance, fields, request):
108
+ instance.save()
109
+
110
+
111
+ class BackgroundAdapter:
112
+ """Implement enqueue and capture/restore_context; see docs/background.md."""
113
+
114
+ def capture_context(self, request):
115
+ return {}
116
+
117
+ def restore_context(self, user, context):
118
+ raise NotImplementedError("Restore a request including tenant and project context")
119
+
120
+ def enqueue(self, operation_id, phase):
121
+ raise NotImplementedError("Durably enqueue modelsheet.background.process(id, phase)")