django-permanent 1.1.6__tar.gz → 2.0.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.
- django_permanent-2.0.0/CHANGES.md +220 -0
- django_permanent-2.0.0/LICENSE +29 -0
- django_permanent-2.0.0/MANIFEST.in +4 -0
- django_permanent-2.0.0/PKG-INFO +277 -0
- django_permanent-2.0.0/README.md +228 -0
- django_permanent-2.0.0/django_permanent/__init__.py +2 -0
- django_permanent-2.0.0/django_permanent/checks.py +58 -0
- django_permanent-2.0.0/django_permanent/deletion.py +159 -0
- {django-permanent-1.1.6 → django_permanent-2.0.0}/django_permanent/managers.py +7 -9
- {django-permanent-1.1.6 → django_permanent-2.0.0}/django_permanent/models.py +10 -15
- {django-permanent-1.1.6 → django_permanent-2.0.0}/django_permanent/query.py +64 -63
- django_permanent-2.0.0/django_permanent/related.py +135 -0
- {django-permanent-1.1.6 → django_permanent-2.0.0}/django_permanent/settings.py +3 -1
- django_permanent-2.0.0/django_permanent/signals.py +4 -0
- django_permanent-2.0.0/django_permanent/tests/cases.py +620 -0
- {django-permanent-1.1.6 → django_permanent-2.0.0}/django_permanent/tests/test_app/models.py +39 -9
- django_permanent-2.0.0/django_permanent.egg-info/PKG-INFO +277 -0
- {django-permanent-1.1.6 → django_permanent-2.0.0}/django_permanent.egg-info/SOURCES.txt +4 -1
- django_permanent-2.0.0/django_permanent.egg-info/requires.txt +10 -0
- {django-permanent-1.1.6 → django_permanent-2.0.0}/setup.cfg +0 -1
- django_permanent-2.0.0/setup.py +48 -0
- django-permanent-1.1.6/MANIFEST.in +0 -2
- django-permanent-1.1.6/PKG-INFO +0 -150
- django-permanent-1.1.6/README.rst +0 -129
- django-permanent-1.1.6/django_permanent/deletion.py +0 -109
- django-permanent-1.1.6/django_permanent/related.py +0 -74
- django-permanent-1.1.6/django_permanent/signals.py +0 -4
- django-permanent-1.1.6/django_permanent/tests/cases.py +0 -305
- django-permanent-1.1.6/django_permanent/tests/test_app/__init__.py +0 -0
- django-permanent-1.1.6/django_permanent.egg-info/PKG-INFO +0 -150
- django-permanent-1.1.6/django_permanent.egg-info/requires.txt +0 -1
- django-permanent-1.1.6/setup.py +0 -40
- {django-permanent-1.1.6 → django_permanent-2.0.0}/django_permanent/tests/__init__.py +0 -0
- {django-permanent-1.1.6/django_permanent → django_permanent-2.0.0/django_permanent/tests/test_app}/__init__.py +0 -0
- {django-permanent-1.1.6 → django_permanent-2.0.0}/django_permanent.egg-info/dependency_links.txt +0 -0
- {django-permanent-1.1.6 → django_permanent-2.0.0}/django_permanent.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
## 2.0.0 (2026-02-07)
|
|
4
|
+
|
|
5
|
+
**Related Issues & PRs from MnogoByte/django-permanent:**
|
|
6
|
+
- Closes MnogoByte/django-permanent#53 - Cannot restore based on removed date
|
|
7
|
+
- Closes MnogoByte/django-permanent#75 - get related object for all_objects
|
|
8
|
+
- Closes MnogoByte/django-permanent#78 - Error on django > 3 when django-utils-six is not installed
|
|
9
|
+
- Closes MnogoByte/django-permanent#79 - Update deletion.py (same as #78)
|
|
10
|
+
- Closes MnogoByte/django-permanent#69 - Installation instructions
|
|
11
|
+
- Closes MnogoByte/django-permanent#66 - How is restore_on_create supposed to work?
|
|
12
|
+
- Closes MnogoByte/django-permanent#67 - README: Improved description
|
|
13
|
+
- Closes MnogoByte/django-permanent#26 - Reorganize generic code (removed 15+ version checks)
|
|
14
|
+
- Closes MnogoByte/django-permanent#18 - Implement future imports (not needed for Python 3.10+)
|
|
15
|
+
|
|
16
|
+
### Breaking Changes
|
|
17
|
+
|
|
18
|
+
- **Minimum Django version: 4.2 LTS** (supports Django 4.2-5.2, experimental support for Django 6.0, tested with Django 7.0 dev)
|
|
19
|
+
- **Minimum Python version: 3.10** (supports 3.10, 3.11, 3.12)
|
|
20
|
+
- Dropped Django 1.x, 2.x, 3.x support
|
|
21
|
+
- Dropped Python 2.7, 3.4-3.9 support
|
|
22
|
+
- Removed django-model-utils dependency
|
|
23
|
+
|
|
24
|
+
### Improvements
|
|
25
|
+
|
|
26
|
+
- Modernized codebase for Django 4.2+
|
|
27
|
+
- Removed Python 2/six dependencies
|
|
28
|
+
- Removed 15+ version compatibility checks
|
|
29
|
+
- Simplified QuerySet and Manager implementations
|
|
30
|
+
- Updated for Django 5.2 API changes (Collector.delete, ForeignObject.get_extra_restriction, Signal)
|
|
31
|
+
- All 44 tests passing with Django 4.2-7.0 dev - 100% pass rate, NO SKIPS! ✅
|
|
32
|
+
- Migrated CI/CD from Travis CI to GitHub Actions
|
|
33
|
+
- Converted documentation from RST to Markdown (README.md, CHANGES.md)
|
|
34
|
+
- Added explicit LICENSE file (BSD)
|
|
35
|
+
|
|
36
|
+
### CI/CD Enhancements
|
|
37
|
+
|
|
38
|
+
- Added automated PyPI publishing workflow (GitHub Actions)
|
|
39
|
+
- Automatically publishes to PyPI when version tag is pushed
|
|
40
|
+
- Creates GitHub releases with release notes
|
|
41
|
+
- Auto-updates CHANGES.md with next version template after release
|
|
42
|
+
- Improved test infrastructure:
|
|
43
|
+
- Added extras_require for test and dev dependencies (coverage, flake8, coveralls)
|
|
44
|
+
- Added TEST_VERBOSITY environment variable support for flexible test output
|
|
45
|
+
- Improved coverage reporting and Coveralls integration
|
|
46
|
+
|
|
47
|
+
### New Features
|
|
48
|
+
|
|
49
|
+
- Added Django System Check (W001) to detect problematic PermanentModel configurations
|
|
50
|
+
- Warns when PermanentModel has CASCADE ForeignKey to non-PermanentModel
|
|
51
|
+
- Helps prevent IntegrityError that occurs when the related non-PermanentModel is deleted
|
|
52
|
+
- Provides helpful suggestions: make related model PermanentModel, use SET_NULL, or use DO_NOTHING
|
|
53
|
+
|
|
54
|
+
### Async Support
|
|
55
|
+
|
|
56
|
+
- **Refactored from thread-local variables to contextvars for async-safe operation**
|
|
57
|
+
- All deletion and query context handling now properly supports asyncio
|
|
58
|
+
- `_show_all_permanent` and `_is_deleting` now use `contextvars.ContextVar` instead of thread locals
|
|
59
|
+
- Context managers `deletion_context()` and `show_all_context()` are fully async-safe
|
|
60
|
+
- Prevents race conditions in async environments (ASGI, async views, async ORM operations)
|
|
61
|
+
- **Django async ORM support**: Works correctly with Django's async QuerySet API (acount, afirst, aget, etc.)
|
|
62
|
+
- **Future-proof**: Ready for Django's continued async expansion in versions 6.0 and 7.0
|
|
63
|
+
|
|
64
|
+
### Bug Fixes from MnogoByte Fork
|
|
65
|
+
|
|
66
|
+
- Fixed MnogoByte/django-permanent#53: `restore()` now correctly respects user filters on `removed` field
|
|
67
|
+
- Previously `QuerySet.filter(removed__range=(start, end)).restore()` would restore ALL deleted objects
|
|
68
|
+
- Now only objects matching the filter criteria are restored
|
|
69
|
+
- Improved `_unpatch()` to distinguish automatic patches from user filters
|
|
70
|
+
- Added test case `test_restore_with_date_filter`
|
|
71
|
+
|
|
72
|
+
- Fixed MnogoByte/django-permanent#75: Access deleted related objects via `show_all_context()`
|
|
73
|
+
- Related objects can now be accessed when soft-deleted using `show_all_context()` context manager
|
|
74
|
+
- Example: `with show_all_context(): obj.foreign_key` now works for deleted related objects
|
|
75
|
+
- Improved `get_queryset_patch()` to respect `show_all_context()` setting
|
|
76
|
+
- Added test case `test_related_object_from_all_objects`
|
|
77
|
+
|
|
78
|
+
- Fixed MnogoByte/django-permanent#78: Removed `django.utils.six` dependency (already fixed by dropping Python 2)
|
|
79
|
+
|
|
80
|
+
- Improved documentation (MnogoByte/django-permanent#69, #66, #67):
|
|
81
|
+
- Added Installation section to README with requirements
|
|
82
|
+
- Improved `restore_on_create` documentation with detailed examples
|
|
83
|
+
- Added section on accessing deleted related objects
|
|
84
|
+
|
|
85
|
+
### Test Changes
|
|
86
|
+
|
|
87
|
+
- Added test_cascade_to_non_permanent_model_warning to verify system check works correctly
|
|
88
|
+
- Removed test_m2m_manager_delete (tested invalid configuration that violates W001 check)
|
|
89
|
+
- Removed test_m2m_select_related (obsolete for Django 1.8+, functionality covered by other tests)
|
|
90
|
+
|
|
91
|
+
### Technical Changes
|
|
92
|
+
|
|
93
|
+
- **Migrated to contextvars** for async-safe context management (replacing thread-local variables)
|
|
94
|
+
- Removed six.iteritems/itervalues usage
|
|
95
|
+
- Updated transaction handling (removed commit_on_success_unless_managed)
|
|
96
|
+
- Fixed Collector.delete return value for Django 1.9+ behavior
|
|
97
|
+
- Updated Signal() to remove deprecated providing_args parameter
|
|
98
|
+
- Updated ForeignObject.get_extra_restriction signature for Django 5.2
|
|
99
|
+
- Updated QuerySet._update() signature to use *args for Django 4.2-5.2 compatibility (returning_fields parameter)
|
|
100
|
+
- Fixed Query.clear_ordering() parameter (force_empty → force)
|
|
101
|
+
- Removed ValuesQuerySet/ValuesListQuerySet custom implementations
|
|
102
|
+
- Simplified manager system (removed PassThroughManager fallback)
|
|
103
|
+
- Added DEFAULT_AUTO_FIELD='django.db.models.BigAutoField' in test configuration for Django 3.2+ compatibility
|
|
104
|
+
- Fixed setup.py to reference README.md instead of README.rst
|
|
105
|
+
|
|
106
|
+
## 1.1.6 (2017-05-04)
|
|
107
|
+
|
|
108
|
+
- Missing related model all_objects manager bug #65 (thanks @kregoslup)
|
|
109
|
+
|
|
110
|
+
## 1.1.5 (2017-03-02)
|
|
111
|
+
|
|
112
|
+
- readme code highlight (thanks @bashu)
|
|
113
|
+
- queryset values method bug #62 (thanks @tjacoel)
|
|
114
|
+
|
|
115
|
+
## 1.1.4 (2016-09-15)
|
|
116
|
+
|
|
117
|
+
- django 1.10 support #58 (thanks @atin65536)
|
|
118
|
+
|
|
119
|
+
## 1.1.3 (2016-09-08)
|
|
120
|
+
|
|
121
|
+
- django 1.9 support #56 (thanks @atin65536)
|
|
122
|
+
- values_list support #56 (thanks @atin65536)
|
|
123
|
+
- QuerySet pickling bug #55 (thanks @atin65536)
|
|
124
|
+
|
|
125
|
+
## 1.1.2 (2016-03-30)
|
|
126
|
+
|
|
127
|
+
- running setup.py without django bug #47
|
|
128
|
+
|
|
129
|
+
## 1.1.1 (2016-03-16)
|
|
130
|
+
|
|
131
|
+
- ForeignKey on_delete behaviour bug (thanks @atin65536)
|
|
132
|
+
|
|
133
|
+
## 1.1.0 (2016-03-16)
|
|
134
|
+
|
|
135
|
+
- Django 1.9 support
|
|
136
|
+
- QuerySetManager support
|
|
137
|
+
- Python 3.5 support
|
|
138
|
+
- Fixed latest supported model_utls version
|
|
139
|
+
- Documentation fixes
|
|
140
|
+
- inf recursion on restore_on_create bug
|
|
141
|
+
- Python 2.6 support dropped
|
|
142
|
+
|
|
143
|
+
Thanks to @aidanlister, @atin65536 and @jarekwg, you are awesome!
|
|
144
|
+
|
|
145
|
+
## 1.0.12 (2015-11-27)
|
|
146
|
+
|
|
147
|
+
- added pre_restore, post_restore signals thanks atin65536
|
|
148
|
+
|
|
149
|
+
## 1.0.11 (2015-05-29)
|
|
150
|
+
|
|
151
|
+
- Fixed deepcopy()-ing PermanentQuerySet #30
|
|
152
|
+
- all_objects.select_related bug #31
|
|
153
|
+
|
|
154
|
+
## 1.0.10 (2015-04-03)
|
|
155
|
+
|
|
156
|
+
- Skip test_m2m_select_related test on Django 1.8 #27
|
|
157
|
+
- Manager isn't available; PermanentModel is abstract #24
|
|
158
|
+
- Atomic only for django >= 1.8 #21
|
|
159
|
+
- Django 1.8 support
|
|
160
|
+
- ReverseSingleRelatedObjectDescriptor patch Bug #25
|
|
161
|
+
- Do not try to restore deleted object if it is created already deleted #23
|
|
162
|
+
|
|
163
|
+
## 1.0.9 (2015-04-02)
|
|
164
|
+
|
|
165
|
+
- Transaction handling backward compatibility #21
|
|
166
|
+
- replaced create_many_related_manager patching with get_extra_restriction patch
|
|
167
|
+
- fixed removable m2m through #22
|
|
168
|
+
|
|
169
|
+
## 1.0.8 (2015-03-27)
|
|
170
|
+
|
|
171
|
+
- Returned force argument
|
|
172
|
+
- Replace commit_on_success_unless_managed by atomic (thanks David Fischer)
|
|
173
|
+
- Find packages recursively (thanks David Fischer)
|
|
174
|
+
- Make setup.py executable (thanks David Fischer)
|
|
175
|
+
|
|
176
|
+
## 1.0.7 (2015-03-24)
|
|
177
|
+
|
|
178
|
+
- Setting trigger field for all removed objects
|
|
179
|
+
- Trigger field model save now affects all objects
|
|
180
|
+
|
|
181
|
+
## 1.0.6 (2015-03-24)
|
|
182
|
+
|
|
183
|
+
- Fixed PermanentModel.restore() fail
|
|
184
|
+
- PermanentModel.delete() now sets removed attribute
|
|
185
|
+
|
|
186
|
+
## 1.0.5 (2015-03-23)
|
|
187
|
+
|
|
188
|
+
- Removed fast_deletes fix
|
|
189
|
+
- create_many_related_manager patch (For proper m2m)
|
|
190
|
+
- Proper Collector patching
|
|
191
|
+
- proper Query patching/unpatching
|
|
192
|
+
- restore_on_create feature
|
|
193
|
+
|
|
194
|
+
## 1.0.4 (2015-03-17)
|
|
195
|
+
|
|
196
|
+
- Many-to-many relations support
|
|
197
|
+
- get_restore_or_create bug
|
|
198
|
+
- added MIDDLEWARE_CLASSES to reduce Django 1.7 output
|
|
199
|
+
|
|
200
|
+
## 1.0.3 (2015-03-17)
|
|
201
|
+
|
|
202
|
+
- Related manager tests
|
|
203
|
+
- Double delete tests
|
|
204
|
+
- Disabled PermanentModels foreign key updates
|
|
205
|
+
- _base_manager override
|
|
206
|
+
- Django 1.7 get_restore_or_create bug
|
|
207
|
+
- Django 1.7 test structure support
|
|
208
|
+
- wrong version in master
|
|
209
|
+
- include tests into the package
|
|
210
|
+
- Fixed get_restore_or_create hardcoded field name
|
|
211
|
+
|
|
212
|
+
## 1.0.2 (2014-02-05)
|
|
213
|
+
|
|
214
|
+
- get_restore_or_create bug
|
|
215
|
+
- Trigger field customisation support
|
|
216
|
+
|
|
217
|
+
## 1.0.1 (2014-02-03)
|
|
218
|
+
|
|
219
|
+
- Prefetch related bug
|
|
220
|
+
- Django 1.6 transactions support
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2013-2026, Alexander Klimenko
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-permanent
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Yet another approach to provide soft (logical) delete or masking (thrashing) django models instead of deleting them physically from db.
|
|
5
|
+
Home-page: https://github.com/meteozond/django-permanent
|
|
6
|
+
Author: Alexander Klimenko
|
|
7
|
+
Author-email: alex@erix.ru
|
|
8
|
+
License: BSD
|
|
9
|
+
Keywords: django,delete,undelete,safedelete,remove,restore,softdelete,logicaldelete,trash
|
|
10
|
+
Classifier: Framework :: Django
|
|
11
|
+
Classifier: Framework :: Django :: 4.2
|
|
12
|
+
Classifier: Framework :: Django :: 5.0
|
|
13
|
+
Classifier: Framework :: Django :: 5.1
|
|
14
|
+
Classifier: Framework :: Django :: 5.2
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: System Administrators
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Software Development
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
24
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: Django<6.0,>=4.2
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: coverage>=7.0; extra == "test"
|
|
31
|
+
Requires-Dist: flake8>=6.0; extra == "test"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: coverage>=7.0; extra == "dev"
|
|
34
|
+
Requires-Dist: flake8>=6.0; extra == "dev"
|
|
35
|
+
Requires-Dist: coveralls>=3.3; extra == "dev"
|
|
36
|
+
Dynamic: author
|
|
37
|
+
Dynamic: author-email
|
|
38
|
+
Dynamic: classifier
|
|
39
|
+
Dynamic: description
|
|
40
|
+
Dynamic: description-content-type
|
|
41
|
+
Dynamic: home-page
|
|
42
|
+
Dynamic: keywords
|
|
43
|
+
Dynamic: license
|
|
44
|
+
Dynamic: license-file
|
|
45
|
+
Dynamic: provides-extra
|
|
46
|
+
Dynamic: requires-dist
|
|
47
|
+
Dynamic: requires-python
|
|
48
|
+
Dynamic: summary
|
|
49
|
+
|
|
50
|
+
# Django Permanent
|
|
51
|
+
|
|
52
|
+
Yet another approach to provide soft (logical) delete or masking (thrashing) django models instead of deleting them physically from db.
|
|
53
|
+
|
|
54
|
+
[](https://github.com/meteozond/django-permanent/actions/workflows/test.yml)
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
Install using pip:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install django-permanent
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or install from source:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git clone https://github.com/meteozond/django-permanent.git
|
|
68
|
+
cd django-permanent
|
|
69
|
+
python setup.py install
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Requirements:**
|
|
73
|
+
- Python 3.10+
|
|
74
|
+
- Django 4.2+
|
|
75
|
+
|
|
76
|
+
## Quick Start
|
|
77
|
+
|
|
78
|
+
To create a non-deletable model just inherit it from `PermanentModel`:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
class MyModel(PermanentModel):
|
|
82
|
+
pass
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
It automatically changes delete behaviour to hide objects instead of deleting them:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
>>> a = MyModel.objects.create(pk=1)
|
|
89
|
+
>>> b = MyModel.objects.create(pk=2)
|
|
90
|
+
>>> MyModel.objects.count()
|
|
91
|
+
2
|
|
92
|
+
>>> a.delete()
|
|
93
|
+
>>> MyModel.objects.count()
|
|
94
|
+
1
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
To recover a deleted object just call its `restore` method:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
>>> a.restore()
|
|
101
|
+
>>> MyModel.objects.count()
|
|
102
|
+
2
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Use the `force` kwarg to enforce physical deletion:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
>>> a.delete(force=True) # Will act as the default django delete
|
|
109
|
+
>>> MyModel._base_manager.count()
|
|
110
|
+
0
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Restore on Create
|
|
114
|
+
|
|
115
|
+
If you want `create()` to restore deleted objects instead of raising an integrity error on unique constraints, use the `restore_on_create` option:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
class Article(PermanentModel):
|
|
119
|
+
title = models.CharField(max_length=100, unique=True)
|
|
120
|
+
|
|
121
|
+
class Permanent:
|
|
122
|
+
restore_on_create = True
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**How it works:**
|
|
126
|
+
|
|
127
|
+
When `restore_on_create = True`, calling `Model.objects.create(**kwargs)` will:
|
|
128
|
+
1. First try to find a matching object (including soft-deleted ones)
|
|
129
|
+
2. If found and deleted: restore it and update with new kwargs
|
|
130
|
+
3. If found and not deleted: return the existing object
|
|
131
|
+
4. If not found: create a new object
|
|
132
|
+
|
|
133
|
+
**Example:**
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
>>> article = Article.objects.create(title="Django Tips")
|
|
137
|
+
>>> article.delete() # Soft delete
|
|
138
|
+
>>> Article.objects.count()
|
|
139
|
+
0
|
|
140
|
+
|
|
141
|
+
# Without restore_on_create: would raise IntegrityError
|
|
142
|
+
# With restore_on_create: restores the deleted article
|
|
143
|
+
>>> article2 = Article.objects.create(title="Django Tips")
|
|
144
|
+
>>> article2.pk == article.pk # Same object!
|
|
145
|
+
True
|
|
146
|
+
>>> Article.objects.count()
|
|
147
|
+
1
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Note:** This feature is most useful for models with unique constraints where you want to "resurrect" deleted objects rather than creating duplicates.
|
|
151
|
+
|
|
152
|
+
## Managers
|
|
153
|
+
|
|
154
|
+
It changes the default model manager to ignore deleted objects, adding a `deleted_objects` manager to see them instead:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
>>> MyModel.objects.count()
|
|
158
|
+
2
|
|
159
|
+
>>> a.delete()
|
|
160
|
+
>>> MyModel.objects.count()
|
|
161
|
+
1
|
|
162
|
+
>>> MyModel.deleted_objects.count()
|
|
163
|
+
1
|
|
164
|
+
>>> MyModel.all_objects.count()
|
|
165
|
+
2
|
|
166
|
+
>>> MyModel._base_manager.count()
|
|
167
|
+
2
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Accessing Deleted Related Objects
|
|
171
|
+
|
|
172
|
+
By default, accessing a foreign key to a deleted object will raise `DoesNotExist`. Use `show_all_context()` to access deleted related objects:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from django_permanent.related import show_all_context
|
|
176
|
+
|
|
177
|
+
# Create models with relationship
|
|
178
|
+
parent = ParentModel.objects.create(name="parent")
|
|
179
|
+
child = ChildModel.objects.create(parent=parent)
|
|
180
|
+
|
|
181
|
+
# Soft delete parent
|
|
182
|
+
parent.delete()
|
|
183
|
+
|
|
184
|
+
# Without show_all_context: raises DoesNotExist
|
|
185
|
+
child.parent # Raises ParentModel.DoesNotExist
|
|
186
|
+
|
|
187
|
+
# With show_all_context: can access deleted parent
|
|
188
|
+
with show_all_context():
|
|
189
|
+
print(child.parent.name) # Works! Returns "parent"
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Note:** This is useful when you need to access relationships to soft-deleted objects, for example in admin interfaces or audit logs.
|
|
193
|
+
|
|
194
|
+
## QuerySet
|
|
195
|
+
|
|
196
|
+
The `QuerySet.delete` method will act as the default django delete, with one exception - objects of models subclassing `PermanentModel` will be marked as deleted; the rest will be deleted physically:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
>>> MyModel.objects.all().delete()
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
You can still force django query set physical deletion:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
>>> MyModel.objects.all().delete(force=True)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Using custom querysets
|
|
209
|
+
|
|
210
|
+
1. Inherit your query set from `PermanentQuerySet`:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
class ServerFileQuerySet(PermanentQuerySet)
|
|
214
|
+
pass
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
2. Wrap `PermanentQuerySet` or `DeletedQuerySet` in you model manager declaration:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
class MyModel(PermanentModel)
|
|
221
|
+
objects = MultiPassThroughManager(ServerFileQuerySet, NonDeletedQuerySet)
|
|
222
|
+
deleted_objects = MultiPassThroughManager(ServerFileQuerySet, DeletedQuerySet)
|
|
223
|
+
all_objects = MultiPassThroughManager(ServerFileQuerySet, PermanentQuerySet)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Method `get_restore_or_create`
|
|
227
|
+
|
|
228
|
+
1. Check for existence of the object.
|
|
229
|
+
2. Restore it if it was deleted.
|
|
230
|
+
3. Create a new one, if it was never created.
|
|
231
|
+
|
|
232
|
+
## Field name
|
|
233
|
+
|
|
234
|
+
The default field named is 'removed', but you can override it with the PERMANENT_FIELD variable in settings.py:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
PERMANENT_FIELD = 'deleted'
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Requirements
|
|
241
|
+
|
|
242
|
+
- Django 4.2+
|
|
243
|
+
- Python 3.10, 3.11, 3.12+
|
|
244
|
+
|
|
245
|
+
## Testing
|
|
246
|
+
|
|
247
|
+
The project uses GitHub Actions for continuous integration.
|
|
248
|
+
|
|
249
|
+
**Run tests locally using act (GitHub Actions locally):**
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
# Install act (macOS)
|
|
253
|
+
brew install act
|
|
254
|
+
|
|
255
|
+
# Run all tests in parallel
|
|
256
|
+
act
|
|
257
|
+
|
|
258
|
+
# Run specific Python version
|
|
259
|
+
act --matrix python-version:3.11
|
|
260
|
+
|
|
261
|
+
# Run specific Python/Django combination
|
|
262
|
+
act --matrix python-version:3.11 --matrix django-version:"Django>=4.2,<5.0"
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Run tests directly:**
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
# Install dependencies
|
|
269
|
+
pip install "Django>=4.2,<5.0" coverage flake8
|
|
270
|
+
|
|
271
|
+
# Run linter
|
|
272
|
+
flake8 django_permanent
|
|
273
|
+
|
|
274
|
+
# Run tests with coverage
|
|
275
|
+
coverage run runtests.py
|
|
276
|
+
coverage report
|
|
277
|
+
```
|