django-guitars 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 (37) hide show
  1. django_guitars-0.1.0/.gitignore +252 -0
  2. django_guitars-0.1.0/CHANGELOG.md +23 -0
  3. django_guitars-0.1.0/LICENSE +21 -0
  4. django_guitars-0.1.0/PKG-INFO +198 -0
  5. django_guitars-0.1.0/README.md +163 -0
  6. django_guitars-0.1.0/core/__init__.py +0 -0
  7. django_guitars-0.1.0/core/asgi.py +17 -0
  8. django_guitars-0.1.0/core/settings.py +50 -0
  9. django_guitars-0.1.0/core/urls.py +6 -0
  10. django_guitars-0.1.0/core/wsgi.py +17 -0
  11. django_guitars-0.1.0/manage.py +23 -0
  12. django_guitars-0.1.0/pyproject.toml +155 -0
  13. django_guitars-0.1.0/src/guitars/__init__.py +1 -0
  14. django_guitars-0.1.0/src/guitars/apps.py +7 -0
  15. django_guitars-0.1.0/src/guitars/management/__init__.py +0 -0
  16. django_guitars-0.1.0/src/guitars/management/commands/__init__.py +0 -0
  17. django_guitars-0.1.0/src/guitars/management/commands/makeguitarmigrations.py +407 -0
  18. django_guitars-0.1.0/src/guitars/migrations/__init__.py +0 -0
  19. django_guitars-0.1.0/src/guitars/models/__init__.py +21 -0
  20. django_guitars-0.1.0/src/guitars/models/base.py +215 -0
  21. django_guitars-0.1.0/src/guitars/models/soft_deletion.py +187 -0
  22. django_guitars-0.1.0/src/guitars/py.typed +0 -0
  23. django_guitars-0.1.0/src/guitars/signals.py +62 -0
  24. django_guitars-0.1.0/src/guitars/sql.py +99 -0
  25. django_guitars-0.1.0/tests/__init__.py +0 -0
  26. django_guitars-0.1.0/tests/settings.py +15 -0
  27. django_guitars-0.1.0/tests/test_base.py +145 -0
  28. django_guitars-0.1.0/tests/test_command.py +56 -0
  29. django_guitars-0.1.0/tests/test_signals.py +49 -0
  30. django_guitars-0.1.0/tests/test_soft_deletion.py +82 -0
  31. django_guitars-0.1.0/tests/testapp/__init__.py +0 -0
  32. django_guitars-0.1.0/tests/testapp/apps.py +6 -0
  33. django_guitars-0.1.0/tests/testapp/migrations/0001_initial.py +64 -0
  34. django_guitars-0.1.0/tests/testapp/migrations/0002_auto_advanced_trigger_function.py +21 -0
  35. django_guitars-0.1.0/tests/testapp/migrations/0003_auto_advanced.py +59 -0
  36. django_guitars-0.1.0/tests/testapp/migrations/__init__.py +0 -0
  37. django_guitars-0.1.0/tests/testapp/models.py +35 -0
@@ -0,0 +1,252 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ *.lcov
51
+ .hypothesis/
52
+ .pytest_cache/
53
+ cover/
54
+
55
+ # Translations
56
+ *.mo
57
+ *.pot
58
+
59
+ # Django stuff:
60
+ *.log
61
+ local_settings.py
62
+ db.sqlite3
63
+ db.sqlite3-journal
64
+
65
+ # Database
66
+ *.db
67
+ *.sqlite3
68
+ /media
69
+ /staticfiles
70
+
71
+ # Security
72
+ .env.local
73
+ .env.production
74
+ secret_key.txt
75
+
76
+ # Static files (collected)
77
+ /static/
78
+ /staticfiles/
79
+
80
+ # Media files
81
+ /media/
82
+
83
+ # Cache
84
+ *.py[cod]
85
+
86
+ # Django migrations (optional - team decision)
87
+ # */migrations/*.py
88
+ # */migrations/*.pyc
89
+
90
+ # Flask stuff:
91
+ instance/
92
+ .webassets-cache
93
+
94
+ # Scrapy stuff:
95
+ .scrapy
96
+
97
+ # Sphinx documentation
98
+ docs/_build/
99
+
100
+ # PyBuilder
101
+ .pybuilder/
102
+ target/
103
+
104
+ # Jupyter Notebook
105
+ .ipynb_checkpoints
106
+
107
+ # IPython
108
+ profile_default/
109
+ ipython_config.py
110
+
111
+ # pyenv
112
+ # For a library or package, you might want to ignore these files since the code is
113
+ # intended to run in multiple environments; otherwise, check them in:
114
+ # .python-version
115
+
116
+ # pipenv
117
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
118
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
119
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
120
+ # install all needed dependencies.
121
+ # Pipfile.lock
122
+
123
+ # UV
124
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
125
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
126
+ # commonly ignored for libraries.
127
+ # uv.lock
128
+
129
+ # poetry
130
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
131
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
132
+ # commonly ignored for libraries.
133
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
134
+ # poetry.lock
135
+ # poetry.toml
136
+
137
+ # pdm
138
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
139
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
140
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
141
+ # pdm.lock
142
+ # pdm.toml
143
+ .pdm-python
144
+ .pdm-build/
145
+
146
+ # pixi
147
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
148
+ # pixi.lock
149
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
150
+ # in the .venv directory. It is recommended not to include this directory in version control.
151
+ .pixi/*
152
+ !.pixi/config.toml
153
+
154
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
155
+ __pypackages__/
156
+
157
+ # Celery stuff
158
+ celerybeat-schedule*
159
+ celerybeat.pid
160
+
161
+ # Redis
162
+ *.rdb
163
+ *.aof
164
+ *.pid
165
+
166
+ # RabbitMQ
167
+ mnesia/
168
+ rabbitmq/
169
+ rabbitmq-data/
170
+
171
+ # ActiveMQ
172
+ activemq-data/
173
+
174
+ # SageMath parsed files
175
+ *.sage.py
176
+
177
+ # Environments
178
+ .env
179
+ .envrc
180
+ .venv
181
+ env/
182
+ venv/
183
+ ENV/
184
+ env.bak/
185
+ venv.bak/
186
+
187
+ # Spyder project settings
188
+ .spyderproject
189
+ .spyproject
190
+
191
+ # Rope project settings
192
+ .ropeproject
193
+
194
+ # mkdocs documentation
195
+ /site
196
+
197
+ # mypy
198
+ .mypy_cache/
199
+ .dmypy.json
200
+ dmypy.json
201
+
202
+ # Pyre type checker
203
+ .pyre/
204
+
205
+ # pytype static type analyzer
206
+ .pytype/
207
+
208
+ # Cython debug symbols
209
+ cython_debug/
210
+
211
+ # PyCharm
212
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
213
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
214
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
215
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
216
+ .idea/
217
+
218
+ # Abstra
219
+ # Abstra is an AI-powered process automation framework.
220
+ # Ignore directories containing user credentials, local state, and settings.
221
+ # Learn more at https://abstra.io/docs
222
+ .abstra/
223
+
224
+ # Visual Studio Code
225
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
226
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
227
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
228
+ # you could uncomment the following to ignore the entire vscode folder
229
+ # .vscode/
230
+ # Temporary file for partial code execution
231
+ tempCodeRunnerFile.py
232
+
233
+ # Ruff stuff:
234
+ .ruff_cache/
235
+
236
+ # PyPI configuration file
237
+ .pypirc
238
+
239
+ # Marimo
240
+ marimo/_static/
241
+ marimo/_lsp/
242
+ __marimo__/
243
+
244
+ # Streamlit
245
+ .streamlit/secrets.toml
246
+
247
+
248
+ .remember/
249
+ .claude/
250
+
251
+ # Project-local pytest junit output (from addopts --junit-xml)
252
+ .pytest-last.xml
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are 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-06-04
9
+
10
+ ### Added
11
+
12
+ - `SetarModel` — base abstract model: DB-default `_created_at` / `_updated_at`
13
+ timestamps, `.update()` / `.aupdate()` helpers, and cached-property
14
+ invalidation on `refresh_from_db()`.
15
+ - `GuitarModel` — `SetarModel` combined with `SoftDeletableModel`.
16
+ - `SoftDeletableModel` with `LiveManager` / `ArchiveManager` /
17
+ `AllObjectsManager` — PostgreSQL-enforced soft deletion, cascade soft delete,
18
+ and `hard_delete()`.
19
+ - `DisableSignals` context manager for temporarily muting Django signals.
20
+ - `makeguitarmigrations` management command — generates the PostgreSQL
21
+ trigger/rule migrations behind the timestamps and soft deletion.
22
+
23
+ [0.1.0]: https://github.com/Behnam-RK/django-guitars/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Behnam RK
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,198 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-guitars
3
+ Version: 0.1.0
4
+ Summary: A reusable Django app for cataloging guitars.
5
+ Project-URL: Homepage, https://github.com/Behnam-RK/django-guitars
6
+ Project-URL: Repository, https://github.com/Behnam-RK/django-guitars
7
+ Project-URL: Issues, https://github.com/Behnam-RK/django-guitars/issues
8
+ Project-URL: Changelog, https://github.com/Behnam-RK/django-guitars/blob/main/CHANGELOG.md
9
+ Author-email: Behnam RK <behnam.rk47@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: django,guitars,reusable-app
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Web Environment
15
+ Classifier: Framework :: Django
16
+ Classifier: Framework :: Django :: 5.0
17
+ Classifier: Framework :: Django :: 5.1
18
+ Classifier: Framework :: Django :: 5.2
19
+ Classifier: Framework :: Django :: 6.0
20
+ Classifier: Intended Audience :: Developers
21
+ Classifier: Operating System :: OS Independent
22
+ Classifier: Programming Language :: Python
23
+ Classifier: Programming Language :: Python :: 3
24
+ Classifier: Programming Language :: Python :: 3 :: Only
25
+ Classifier: Programming Language :: Python :: 3.10
26
+ Classifier: Programming Language :: Python :: 3.11
27
+ Classifier: Programming Language :: Python :: 3.12
28
+ Classifier: Programming Language :: Python :: 3.13
29
+ Classifier: Programming Language :: Python :: 3.14
30
+ Classifier: Topic :: Internet :: WWW/HTTP
31
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
32
+ Requires-Python: >=3.10
33
+ Requires-Dist: django>=5.0
34
+ Description-Content-Type: text/markdown
35
+
36
+ # django-guitars
37
+
38
+ > Django Reinhardt was one of the greatest guitarists who ever lived — so
39
+ > **django-guitars** hands *the other* Django, the web framework, a better set
40
+ > of guitars.
41
+
42
+ A small, focused collection of **reusable Django utilities**: opinionated base
43
+ models, PostgreSQL-enforced soft deletion, and the helpers that make them work.
44
+ Use only the pieces you need.
45
+
46
+ [![PyPI version](https://img.shields.io/pypi/v/django-guitars.svg)](https://pypi.org/project/django-guitars/)
47
+ [![Python versions](https://img.shields.io/pypi/pyversions/django-guitars.svg)](https://pypi.org/project/django-guitars/)
48
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
49
+
50
+ ## Requirements
51
+
52
+ - **Python** ≥ 3.10
53
+ - **Django** ≥ 5.0 — uses `db_default`
54
+ - **PostgreSQL** — soft deletion and the `updated_at` refresh are enforced by
55
+ PostgreSQL rules and triggers
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ pip install django-guitars
61
+ ```
62
+
63
+ Add the app to your settings:
64
+
65
+ ```python
66
+ INSTALLED_APPS = [
67
+ # ...
68
+ "guitars",
69
+ ]
70
+ ```
71
+
72
+ ## What's inside
73
+
74
+ ### Base models
75
+
76
+ | Base | What you get |
77
+ | --- | --- |
78
+ | `SetarModel` | `_created_at` / `_updated_at` (DB-default `NOW()`; `_updated_at` kept current by a PostgreSQL statement trigger, so it's accurate even for bulk/raw updates), `.update()` / `.aupdate()`, cached-property invalidation on `refresh_from_db()`, and `app_label()` / `model_name()` / `class_name()` helpers |
79
+ | `GuitarModel` | Everything in `SetarModel` **plus** soft deletion (it is `SetarModel` + `SoftDeletableModel`) |
80
+ | `SoftDeletableModel` | Soft deletion on its own |
81
+
82
+ ```python
83
+ from django.db import models
84
+
85
+ from guitars.models import GuitarModel
86
+
87
+
88
+ class Article(GuitarModel):
89
+ title = models.CharField(max_length=200)
90
+ ```
91
+
92
+ The `.update()` helper sets attributes and saves in one call:
93
+
94
+ ```python
95
+ article.update(title="New title") # set fields + save (only changed fields)
96
+ article.update(title="x", _save=False) # change in memory only, no DB write
97
+ await article.aupdate(title="async") # async variant
98
+ ```
99
+
100
+ ### Soft deletion
101
+
102
+ For models inheriting `SoftDeletableModel` (or `GuitarModel`), `.delete()`
103
+ becomes a **soft delete**: the row stays and `_deleted_at` is set. Because this
104
+ is enforced by a PostgreSQL rule, it holds even for queryset bulk deletes and
105
+ raw SQL. Three managers expose the data:
106
+
107
+ ```python
108
+ Article.objects.all() # live rows only (the default manager)
109
+ Article._archives.all() # soft-deleted rows only
110
+ Article._all_objects.all() # everything
111
+
112
+ article.delete() # soft delete — sets _deleted_at
113
+ article.is_deleted # True
114
+ article.is_alive # False
115
+
116
+ # Permanently remove rows (and CASCADE-related rows), bypassing the rule:
117
+ Article._all_objects.filter(...).hard_delete()
118
+ ```
119
+
120
+ Soft-deleting a row also soft-deletes rows related by `on_delete=CASCADE`.
121
+
122
+ > ⚠️ **Required setup.** The soft-delete rule (and the `updated_at` trigger)
123
+ > live in a migration generated by [`makeguitarmigrations`](#makeguitarmigrations).
124
+ > Until you run that command and `migrate`, **`.delete()` permanently deletes the
125
+ > row** — the protection isn't active yet. Re-run it whenever you add or change a
126
+ > model that uses these bases.
127
+
128
+ ### `makeguitarmigrations`
129
+
130
+ `makemigrations` does **not** create the triggers and rules — they live in
131
+ separate migrations generated by this command, and this step is **required** for
132
+ soft deletion and the `updated_at` trigger to work. After your usual
133
+ `makemigrations`, run:
134
+
135
+ ```bash
136
+ python manage.py makeguitarmigrations
137
+ ```
138
+
139
+ It scans your **first-party** apps for models with `_updated_at` / `_deleted_at`
140
+ and writes the matching trigger/rule migrations. Tell it which apps are yours:
141
+
142
+ ```python
143
+ # settings.py
144
+ LOCAL_APPS = ["blog", "shop"] # apps the command scans
145
+
146
+ # Optional: which app hosts the shared trigger-function migration.
147
+ # Defaults to LOCAL_APPS[0].
148
+ # TRIGGER_FUNCTION_APP = "blog"
149
+ ```
150
+
151
+ Use `--check` in CI to fail when advanced migrations are missing:
152
+
153
+ ```bash
154
+ python manage.py makeguitarmigrations --check
155
+ ```
156
+
157
+ ### `DisableSignals`
158
+
159
+ A context manager that temporarily disconnects Django signals — handy for bulk
160
+ imports or silent saves:
161
+
162
+ ```python
163
+ from django.db.models.signals import post_save
164
+
165
+ from guitars.signals import DisableSignals
166
+
167
+ with DisableSignals(): # all default signals
168
+ instance.save() # nothing fires
169
+
170
+ with DisableSignals(signals=[post_save]): # only the listed signals
171
+ instance.save()
172
+ ```
173
+
174
+ ## Development
175
+
176
+ Requires [uv](https://docs.astral.sh/uv/) and Docker (for PostgreSQL).
177
+
178
+ ```bash
179
+ uv sync # install dependencies + the package (editable)
180
+ docker compose up -d # start PostgreSQL (skip if you already run one on :5432)
181
+ uv run pytest # run the test suite
182
+ uv run pytest --cov=guitars --cov-report=term-missing
183
+ ```
184
+
185
+ The test suite defines concrete models in `tests/testapp` (the shipped package
186
+ is abstract-only) and runs against a real PostgreSQL database so the rules and
187
+ triggers are actually exercised.
188
+
189
+ ## License
190
+
191
+ [MIT](LICENSE) © 2026 Behnam RK
192
+
193
+ ## Why "guitars"?
194
+
195
+ Django Reinhardt was a legendary jazz guitarist. This package gives the *other*
196
+ Django — the web framework — some better guitars: a grab-bag of utilities that
197
+ make everyday Django a little nicer. The base models keep the theme going with
198
+ `SetarModel` and `GuitarModel`.
@@ -0,0 +1,163 @@
1
+ # django-guitars
2
+
3
+ > Django Reinhardt was one of the greatest guitarists who ever lived — so
4
+ > **django-guitars** hands *the other* Django, the web framework, a better set
5
+ > of guitars.
6
+
7
+ A small, focused collection of **reusable Django utilities**: opinionated base
8
+ models, PostgreSQL-enforced soft deletion, and the helpers that make them work.
9
+ Use only the pieces you need.
10
+
11
+ [![PyPI version](https://img.shields.io/pypi/v/django-guitars.svg)](https://pypi.org/project/django-guitars/)
12
+ [![Python versions](https://img.shields.io/pypi/pyversions/django-guitars.svg)](https://pypi.org/project/django-guitars/)
13
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
14
+
15
+ ## Requirements
16
+
17
+ - **Python** ≥ 3.10
18
+ - **Django** ≥ 5.0 — uses `db_default`
19
+ - **PostgreSQL** — soft deletion and the `updated_at` refresh are enforced by
20
+ PostgreSQL rules and triggers
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install django-guitars
26
+ ```
27
+
28
+ Add the app to your settings:
29
+
30
+ ```python
31
+ INSTALLED_APPS = [
32
+ # ...
33
+ "guitars",
34
+ ]
35
+ ```
36
+
37
+ ## What's inside
38
+
39
+ ### Base models
40
+
41
+ | Base | What you get |
42
+ | --- | --- |
43
+ | `SetarModel` | `_created_at` / `_updated_at` (DB-default `NOW()`; `_updated_at` kept current by a PostgreSQL statement trigger, so it's accurate even for bulk/raw updates), `.update()` / `.aupdate()`, cached-property invalidation on `refresh_from_db()`, and `app_label()` / `model_name()` / `class_name()` helpers |
44
+ | `GuitarModel` | Everything in `SetarModel` **plus** soft deletion (it is `SetarModel` + `SoftDeletableModel`) |
45
+ | `SoftDeletableModel` | Soft deletion on its own |
46
+
47
+ ```python
48
+ from django.db import models
49
+
50
+ from guitars.models import GuitarModel
51
+
52
+
53
+ class Article(GuitarModel):
54
+ title = models.CharField(max_length=200)
55
+ ```
56
+
57
+ The `.update()` helper sets attributes and saves in one call:
58
+
59
+ ```python
60
+ article.update(title="New title") # set fields + save (only changed fields)
61
+ article.update(title="x", _save=False) # change in memory only, no DB write
62
+ await article.aupdate(title="async") # async variant
63
+ ```
64
+
65
+ ### Soft deletion
66
+
67
+ For models inheriting `SoftDeletableModel` (or `GuitarModel`), `.delete()`
68
+ becomes a **soft delete**: the row stays and `_deleted_at` is set. Because this
69
+ is enforced by a PostgreSQL rule, it holds even for queryset bulk deletes and
70
+ raw SQL. Three managers expose the data:
71
+
72
+ ```python
73
+ Article.objects.all() # live rows only (the default manager)
74
+ Article._archives.all() # soft-deleted rows only
75
+ Article._all_objects.all() # everything
76
+
77
+ article.delete() # soft delete — sets _deleted_at
78
+ article.is_deleted # True
79
+ article.is_alive # False
80
+
81
+ # Permanently remove rows (and CASCADE-related rows), bypassing the rule:
82
+ Article._all_objects.filter(...).hard_delete()
83
+ ```
84
+
85
+ Soft-deleting a row also soft-deletes rows related by `on_delete=CASCADE`.
86
+
87
+ > ⚠️ **Required setup.** The soft-delete rule (and the `updated_at` trigger)
88
+ > live in a migration generated by [`makeguitarmigrations`](#makeguitarmigrations).
89
+ > Until you run that command and `migrate`, **`.delete()` permanently deletes the
90
+ > row** — the protection isn't active yet. Re-run it whenever you add or change a
91
+ > model that uses these bases.
92
+
93
+ ### `makeguitarmigrations`
94
+
95
+ `makemigrations` does **not** create the triggers and rules — they live in
96
+ separate migrations generated by this command, and this step is **required** for
97
+ soft deletion and the `updated_at` trigger to work. After your usual
98
+ `makemigrations`, run:
99
+
100
+ ```bash
101
+ python manage.py makeguitarmigrations
102
+ ```
103
+
104
+ It scans your **first-party** apps for models with `_updated_at` / `_deleted_at`
105
+ and writes the matching trigger/rule migrations. Tell it which apps are yours:
106
+
107
+ ```python
108
+ # settings.py
109
+ LOCAL_APPS = ["blog", "shop"] # apps the command scans
110
+
111
+ # Optional: which app hosts the shared trigger-function migration.
112
+ # Defaults to LOCAL_APPS[0].
113
+ # TRIGGER_FUNCTION_APP = "blog"
114
+ ```
115
+
116
+ Use `--check` in CI to fail when advanced migrations are missing:
117
+
118
+ ```bash
119
+ python manage.py makeguitarmigrations --check
120
+ ```
121
+
122
+ ### `DisableSignals`
123
+
124
+ A context manager that temporarily disconnects Django signals — handy for bulk
125
+ imports or silent saves:
126
+
127
+ ```python
128
+ from django.db.models.signals import post_save
129
+
130
+ from guitars.signals import DisableSignals
131
+
132
+ with DisableSignals(): # all default signals
133
+ instance.save() # nothing fires
134
+
135
+ with DisableSignals(signals=[post_save]): # only the listed signals
136
+ instance.save()
137
+ ```
138
+
139
+ ## Development
140
+
141
+ Requires [uv](https://docs.astral.sh/uv/) and Docker (for PostgreSQL).
142
+
143
+ ```bash
144
+ uv sync # install dependencies + the package (editable)
145
+ docker compose up -d # start PostgreSQL (skip if you already run one on :5432)
146
+ uv run pytest # run the test suite
147
+ uv run pytest --cov=guitars --cov-report=term-missing
148
+ ```
149
+
150
+ The test suite defines concrete models in `tests/testapp` (the shipped package
151
+ is abstract-only) and runs against a real PostgreSQL database so the rules and
152
+ triggers are actually exercised.
153
+
154
+ ## License
155
+
156
+ [MIT](LICENSE) © 2026 Behnam RK
157
+
158
+ ## Why "guitars"?
159
+
160
+ Django Reinhardt was a legendary jazz guitarist. This package gives the *other*
161
+ Django — the web framework — some better guitars: a grab-bag of utilities that
162
+ make everyday Django a little nicer. The base models keep the theme going with
163
+ `SetarModel` and `GuitarModel`.
File without changes
@@ -0,0 +1,17 @@
1
+ """
2
+ ASGI config for guitars project.
3
+
4
+ It exposes the ASGI callable as a module-level variable named ``application``.
5
+
6
+ For more information on this file, see
7
+ https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
8
+ """
9
+
10
+ import os
11
+
12
+ from django.core.asgi import get_asgi_application
13
+
14
+
15
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
16
+
17
+ application = get_asgi_application()