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.
Files changed (36) hide show
  1. django_permanent-2.0.0/CHANGES.md +220 -0
  2. django_permanent-2.0.0/LICENSE +29 -0
  3. django_permanent-2.0.0/MANIFEST.in +4 -0
  4. django_permanent-2.0.0/PKG-INFO +277 -0
  5. django_permanent-2.0.0/README.md +228 -0
  6. django_permanent-2.0.0/django_permanent/__init__.py +2 -0
  7. django_permanent-2.0.0/django_permanent/checks.py +58 -0
  8. django_permanent-2.0.0/django_permanent/deletion.py +159 -0
  9. {django-permanent-1.1.6 → django_permanent-2.0.0}/django_permanent/managers.py +7 -9
  10. {django-permanent-1.1.6 → django_permanent-2.0.0}/django_permanent/models.py +10 -15
  11. {django-permanent-1.1.6 → django_permanent-2.0.0}/django_permanent/query.py +64 -63
  12. django_permanent-2.0.0/django_permanent/related.py +135 -0
  13. {django-permanent-1.1.6 → django_permanent-2.0.0}/django_permanent/settings.py +3 -1
  14. django_permanent-2.0.0/django_permanent/signals.py +4 -0
  15. django_permanent-2.0.0/django_permanent/tests/cases.py +620 -0
  16. {django-permanent-1.1.6 → django_permanent-2.0.0}/django_permanent/tests/test_app/models.py +39 -9
  17. django_permanent-2.0.0/django_permanent.egg-info/PKG-INFO +277 -0
  18. {django-permanent-1.1.6 → django_permanent-2.0.0}/django_permanent.egg-info/SOURCES.txt +4 -1
  19. django_permanent-2.0.0/django_permanent.egg-info/requires.txt +10 -0
  20. {django-permanent-1.1.6 → django_permanent-2.0.0}/setup.cfg +0 -1
  21. django_permanent-2.0.0/setup.py +48 -0
  22. django-permanent-1.1.6/MANIFEST.in +0 -2
  23. django-permanent-1.1.6/PKG-INFO +0 -150
  24. django-permanent-1.1.6/README.rst +0 -129
  25. django-permanent-1.1.6/django_permanent/deletion.py +0 -109
  26. django-permanent-1.1.6/django_permanent/related.py +0 -74
  27. django-permanent-1.1.6/django_permanent/signals.py +0 -4
  28. django-permanent-1.1.6/django_permanent/tests/cases.py +0 -305
  29. django-permanent-1.1.6/django_permanent/tests/test_app/__init__.py +0 -0
  30. django-permanent-1.1.6/django_permanent.egg-info/PKG-INFO +0 -150
  31. django-permanent-1.1.6/django_permanent.egg-info/requires.txt +0 -1
  32. django-permanent-1.1.6/setup.py +0 -40
  33. {django-permanent-1.1.6 → django_permanent-2.0.0}/django_permanent/tests/__init__.py +0 -0
  34. {django-permanent-1.1.6/django_permanent → django_permanent-2.0.0/django_permanent/tests/test_app}/__init__.py +0 -0
  35. {django-permanent-1.1.6 → django_permanent-2.0.0}/django_permanent.egg-info/dependency_links.txt +0 -0
  36. {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,4 @@
1
+ include README.md
2
+ include LICENSE
3
+ include CHANGES.md
4
+ recursive-include django_permanent *.py
@@ -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
+ [![Tests](https://github.com/meteozond/django-permanent/actions/workflows/test.yml/badge.svg)](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
+ ```