From 9ea1073b97bfe8b2af7f4a1c3761e85939a509b6 Mon Sep 17 00:00:00 2001 From: Mario Rosa Date: Sun, 18 Sep 2022 09:44:25 -0400 Subject: [PATCH 01/12] initial changes for 7.0 --- .github/workflows/main.yml | 22 +++++++++------------- CHANGELOG.md | 8 ++++++++ README.rst | 4 ++-- django_cleanup/__init__.py | 5 +---- setup.py | 5 ++--- tox.ini | 7 ++----- 6 files changed, 24 insertions(+), 27 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f1e818d..f268372 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,19 +12,6 @@ jobs: strategy: matrix: include: - - python: "3.5" - toxenv: py35-django22 - - python: "3.6" - toxenv: py36-django22 - - python: "3.7" - toxenv: py37-django22 - - python: "3.8" - toxenv: py38-django22 - - python: "3.9" - toxenv: py39-django22 - - python: "pypy-3.8" - toxenv: pypy3-django22 - - python: "3.6" toxenv: py36-django32 - python: "3.7" @@ -46,6 +33,15 @@ jobs: toxenv: py310-django40 - python: "pypy-3.8" toxenv: pypy3-django40 + + - python: "3.8" + toxenv: py38-django41 + - python: "3.9" + toxenv: py39-django41 + - python: "3.10" + toxenv: py310-django41 + - python: "pypy-3.8" + toxenv: pypy3-django41 steps: - uses: actions/checkout@v2 - name: Setup Python diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d4fe2a..ae1209b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [7.0.0] - TBD +### Added +- Update to run tests for django 4.1. + +### Removed +- Dropped support for django 2.2 and python 3.5. + ## [6.0.0] - 2022-01-24 ### Added - Update to run tests for python 3.10. PR [#88] from [@johnthagen](https://github.com/johnthagen). @@ -70,6 +77,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2012-08-14 [Unreleased]: https://github.com/un1t/django-cleanup/compare/6.0.0...HEAD +[7.0.0]: https://github.com/un1t/django-cleanup/compare/6.0.0...7.0.0 [6.0.0]: https://github.com/un1t/django-cleanup/compare/5.2.0...6.0.0 [5.2.0]: https://github.com/un1t/django-cleanup/compare/5.1.0...5.2.0 [5.1.0]: https://github.com/un1t/django-cleanup/compare/5.0.0...5.1.0 diff --git a/README.rst b/README.rst index 24ad841..de673dc 100644 --- a/README.rst +++ b/README.rst @@ -11,8 +11,8 @@ is set as the :code:`FileField`'s default value will not be deleted. Compatibility ------------- -- Django 2.2, 3.2, 4.0 (`See Django Supported Versions `_) -- Python 3.5+ +- Django 3.2, 4.0, 4.1 (`See Django Supported Versions `_) +- Python 3.6+ - Compatible with `sorl-thumbnail `_ - Compatible with `easy-thumbnail `_ diff --git a/django_cleanup/__init__.py b/django_cleanup/__init__.py index bc9786d..a5b349f 100644 --- a/django_cleanup/__init__.py +++ b/django_cleanup/__init__.py @@ -3,8 +3,5 @@ subclasses. It will delete old files when a new file is being save and it will delete files on model instance deletion. ''' -import django -__version__ = '6.0.0' -if django.VERSION < (3, 2): - default_app_config = 'django_cleanup.apps.CleanupConfig' +__version__ = '7.0.0-dev0001' diff --git a/setup.py b/setup.py index 1590e0c..34d9e8a 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ def find_version(*parts): version=find_version('django_cleanup', '__init__.py'), packages=['django_cleanup'], include_package_data=True, - requires=['python (>=3.5)', 'django (>=2.2)'], + requires=['python (>=3.6)', 'django (>=3.2)'], description='Deletes old files.', long_description=read('README.rst'), long_description_content_type='text/x-rst', @@ -46,15 +46,14 @@ def find_version(*parts): 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Framework :: Django', - 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.2', 'Framework :: Django :: 4.0', + 'Framework :: Django :: 4.1', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', diff --git a/tox.ini b/tox.ini index b038b68..7a54bc2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,8 @@ [tox] envlist = - py{35,36,37,38,39,py3}-django22 py{36,37,38,39,310,py3}-django32 - py{38,39,310,py3}-django40 - py{38,39,310,py3}-django{main} + py{38,39,310,py3}-django4{0,1} + py{38,39,310,311,py3}-django{main} [testenv] deps = djangomain: https://github.com/django/django/tarball/main @@ -15,7 +14,5 @@ deps = django40: django<4.1 # LTS April 2021 - April 2024 django32: django<3.3 - # LTS April 2019 - April 2022 - django22: django<2.3 -rdjango_cleanup/testapp/requirements.txt commands=pytest #-k "test_name" From 2e3e784913ab82bdd28e8b1bba77439987d3aed8 Mon Sep 17 00:00:00 2001 From: Mario Rosa Date: Sun, 18 Sep 2022 13:32:13 -0400 Subject: [PATCH 02/12] add select mode --- CHANGELOG.md | 1 + README.rst | 16 ++++++++++++++++ django_cleanup/apps.py | 9 +++++++++ django_cleanup/cache.py | 14 +++++++++----- django_cleanup/cleanup.py | 11 +++++++++-- 5 files changed, 44 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae1209b..7a029bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [7.0.0] - TBD ### Added - Update to run tests for django 4.1. +- Select mode, with the ability to only cleanup selected models using a `select` decorator. Resolves issue [#75]. ### Removed - Dropped support for django 2.2 and python 3.5. diff --git a/README.rst b/README.rst index de673dc..8f7d71e 100644 --- a/README.rst +++ b/README.rst @@ -125,6 +125,22 @@ Ignore a model and do not perform cleanup when the model is deleted or its files class MyModel(models.Model): image = models.FileField() +Only cleanup selected models +---------------------------- +If you have many models to ignore, or if you prefer to be explicit about what models are selected, +you can change the mode of django-cleanup to select mode by using the select mode app config. In +your ``INSTALLED_APPS`` setting you will replace ``'django_cleanup.apps.CleanupConfig'`` +with ``'django_cleanup.apps.CleanupSelectedConfig'``. Then use the ``select`` decorator to mark a +model for cleanup: + +.. code-block:: py + + from django_cleanup import cleanup + + @cleanup.select + class MyModel(models.Model): + image = models.FileField() + How to run tests ================ Install, setup and use pyenv_ to install all the required versions of cPython diff --git a/django_cleanup/apps.py b/django_cleanup/apps.py index fd22910..1a8a709 100644 --- a/django_cleanup/apps.py +++ b/django_cleanup/apps.py @@ -9,7 +9,16 @@ class CleanupConfig(AppConfig): name = 'django_cleanup' verbose_name = 'Django Cleanup' + default = True def ready(self): cache.prepare() handlers.connect() + +class CleanupSelectedConfig(AppConfig): + name = 'django_cleanup' + verbose_name = 'Django Cleanup' + + def ready(self): + cache.prepare(True) + handlers.connect() diff --git a/django_cleanup/cache.py b/django_cleanup/cache.py index b34fcf9..692fc2a 100644 --- a/django_cleanup/cache.py +++ b/django_cleanup/cache.py @@ -24,13 +24,13 @@ def fields_dict_default(): # cache init ## -def prepare(): +def prepare(select_mode=False): '''Prepare the cache for all models, non-reentrant''' if FIELDS: # pragma: no cover return for model in apps.get_models(): - if ignore_model(model): + if ignore_model(model, select_mode): continue name = get_model_name(model) if model_has_filefields(name): # pragma: no cover @@ -113,10 +113,14 @@ def get_model_name(model): def get_mangled_ignore(model): - '''returns a mangled attribute name specific to the model''' + '''returns a mangled attribute name specific to the model for ignore functionality''' return '_{opt.model_name}__{opt.app_label}_cleanup_ignore'.format(opt=model._meta) +def get_mangled_select(model): + '''returns a mangled attribute name specific to the model for select functionality''' + return '_{opt.model_name}__{opt.app_label}_cleanup_select'.format(opt=model._meta) + # booleans ## @@ -125,9 +129,9 @@ def model_has_filefields(model_name): return model_name in FIELDS -def ignore_model(model): +def ignore_model(model, select_mode): '''Check if a model should be ignored''' - return hasattr(model, get_mangled_ignore(model)) + return not hasattr(model, get_mangled_select(model)) if select_mode else hasattr(model, get_mangled_ignore(model)) # instance functions ## diff --git a/django_cleanup/cleanup.py b/django_cleanup/cleanup.py index ad7a163..86bacb8 100644 --- a/django_cleanup/cleanup.py +++ b/django_cleanup/cleanup.py @@ -1,9 +1,9 @@ '''Public utilities''' from .cache import ( - get_mangled_ignore as _get_mangled_ignore, make_cleanup_cache as _make_cleanup_cache) + get_mangled_ignore as _get_mangled_ignore, get_mangled_select as _get_mangled_select, make_cleanup_cache as _make_cleanup_cache) -__all__ = ['refresh', 'cleanup_ignore'] +__all__ = ['refresh', 'cleanup_ignore', 'cleanup_select'] def refresh(instance): @@ -16,3 +16,10 @@ def ignore(cls): setattr(cls, _get_mangled_ignore(cls), None) return cls cleanup_ignore = ignore + + +def select(cls): + '''Mark a model to select for cleanup''' + setattr(cls, _get_mangled_select(cls), None) + return cls +cleanup_select = select From 86fb8f31a05963c96edeb3e723fc7e0d5e051a5e Mon Sep 17 00:00:00 2001 From: Mario Rosa Date: Wed, 21 Dec 2022 16:56:59 -0500 Subject: [PATCH 03/12] add pythong 3.11 --- .github/workflows/main.yml | 2 ++ CHANGELOG.md | 1 + setup.py | 1 + tox.ini | 4 ++-- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f268372..41e2334 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,6 +40,8 @@ jobs: toxenv: py39-django41 - python: "3.10" toxenv: py310-django41 + - python: "3.11" + toxenv: py310-django41 - python: "pypy-3.8" toxenv: pypy3-django41 steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a029bd..7b98cb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [7.0.0] - TBD ### Added - Update to run tests for django 4.1. +- Update to run tests on python 3.11 with django 4.1. - Select mode, with the ability to only cleanup selected models using a `select` decorator. Resolves issue [#75]. ### Removed diff --git a/setup.py b/setup.py index 34d9e8a..2b4446c 100644 --- a/setup.py +++ b/setup.py @@ -59,6 +59,7 @@ def find_version(*parts): 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Utilities', diff --git a/tox.ini b/tox.ini index 7a54bc2..7a4dc93 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,8 @@ [tox] envlist = py{36,37,38,39,310,py3}-django32 - py{38,39,310,py3}-django4{0,1} - py{38,39,310,311,py3}-django{main} + py{38,39,310,py3}-django40 + py{38,39,310,311,py3}-django{1, main} [testenv] deps = djangomain: https://github.com/django/django/tarball/main From 2cce22bca09e5a26dcac61e0053086145c9742ba Mon Sep 17 00:00:00 2001 From: Mario Rosa Date: Wed, 21 Dec 2022 17:03:53 -0500 Subject: [PATCH 04/12] try putting python 2.6 on ubuntu 20.04 --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 41e2334..a44077e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,6 +14,7 @@ jobs: include: - python: "3.6" toxenv: py36-django32 + os: ubuntu-20.04 - python: "3.7" toxenv: py37-django32 - python: "3.8" From d19c2eac813dd5bb415c87a3da1b36ce0a47bb64 Mon Sep 17 00:00:00 2001 From: Mario Rosa Date: Wed, 21 Dec 2022 18:00:35 -0500 Subject: [PATCH 05/12] propagate value from matrix with fallback --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a44077e..0e07550 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os || 'ubuntu-latest' }} strategy: matrix: include: From cd76d0411e9ce4e0d02402beef6216e62ec84d4d Mon Sep 17 00:00:00 2001 From: Mario Rosa Date: Sun, 8 Jan 2023 11:38:30 -0500 Subject: [PATCH 06/12] update structure and add selected config tests --- django_cleanup/testapp/testing_helpers.py | 34 ------------ pytest.ini | 8 +-- setup.py | 3 +- .../django_cleanup}/__init__.py | 0 .../django_cleanup}/apps.py | 0 .../django_cleanup}/cache.py | 2 +- .../django_cleanup}/cleanup.py | 3 +- .../django_cleanup}/handlers.py | 0 .../django_cleanup}/signals.py | 0 {django_cleanup/testapp => test}/__init__.py | 0 test/conftest.py | 49 ++++++++++++++++++ .../testapp => test}/media/pic.jpg | Bin .../testapp => test}/models/__init__.py | 0 .../testapp => test}/models/app.py | 11 ++-- .../testapp => test}/models/integration.py | 4 +- test/pytest_plugin/__init__.py | 9 ++++ .../testapp => test}/requirements.txt | 1 + {django_cleanup/testapp => test}/settings.py | 2 +- {django_cleanup/testapp => test}/storage.py | 0 {django_cleanup/testapp => test}/test_all.py | 40 ++++++++++++-- .../testapp => test}/test_integration.py | 10 ++-- test/testing_helpers.py | 13 +++++ tox.ini | 6 +-- 23 files changed, 135 insertions(+), 60 deletions(-) delete mode 100644 django_cleanup/testapp/testing_helpers.py rename {django_cleanup => src/django_cleanup}/__init__.py (100%) rename {django_cleanup => src/django_cleanup}/apps.py (100%) rename {django_cleanup => src/django_cleanup}/cache.py (97%) rename {django_cleanup => src/django_cleanup}/cleanup.py (88%) rename {django_cleanup => src/django_cleanup}/handlers.py (100%) rename {django_cleanup => src/django_cleanup}/signals.py (100%) rename {django_cleanup/testapp => test}/__init__.py (100%) create mode 100644 test/conftest.py rename {django_cleanup/testapp => test}/media/pic.jpg (100%) rename {django_cleanup/testapp => test}/models/__init__.py (100%) rename {django_cleanup/testapp => test}/models/app.py (77%) rename {django_cleanup/testapp => test}/models/integration.py (74%) create mode 100644 test/pytest_plugin/__init__.py rename {django_cleanup/testapp => test}/requirements.txt (87%) rename {django_cleanup/testapp => test}/settings.py (95%) rename {django_cleanup/testapp => test}/storage.py (100%) rename {django_cleanup/testapp => test}/test_all.py (90%) rename {django_cleanup/testapp => test}/test_integration.py (90%) create mode 100644 test/testing_helpers.py diff --git a/django_cleanup/testapp/testing_helpers.py b/django_cleanup/testapp/testing_helpers.py deleted file mode 100644 index d5f5bbf..0000000 --- a/django_cleanup/testapp/testing_helpers.py +++ /dev/null @@ -1,34 +0,0 @@ -import os -import random -import shutil -import string - -from django.conf import settings -from django.db import router - -import pytest - - -def get_using(instance): - return router.db_for_write(instance.__class__, instance=instance) - - -def get_random_pic_name(length=20): - return 'pic{}.jpg'.format( - ''.join(random.choice(string.ascii_letters) for m in range(length))) - - -@pytest.fixture(params=[settings.MEDIA_ROOT]) -def picture(request): - src = os.path.join(request.param, 'pic.jpg') - dst = os.path.join(request.param, get_random_pic_name()) - shutil.copyfile(src, dst) - try: - yield { - 'path': dst, - 'filename': os.path.split(dst)[-1], - 'srcpath': src - } - finally: - if os.path.exists(dst): - os.remove(dst) diff --git a/pytest.ini b/pytest.ini index 69769da..a4ce5c5 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,6 @@ [pytest] -DJANGO_SETTINGS_MODULE=django_cleanup.testapp.settings -python_paths=. -addopts = -n auto -v --cov-report=term-missing --cov=django_cleanup django_cleanup +DJANGO_SETTINGS_MODULE = test.settings +pythonpath = . src +addopts = -n auto -v --cov-report=term-missing --cov=django_cleanup --forked +markers = + CleanupSelectedConfig: marks test as using the CleanupSelectedConfig app config diff --git a/setup.py b/setup.py index 2b4446c..cd318db 100644 --- a/setup.py +++ b/setup.py @@ -29,8 +29,9 @@ def find_version(*parts): setup( name='django-cleanup', - version=find_version('django_cleanup', '__init__.py'), + version=find_version('src/django_cleanup', '__init__.py'), packages=['django_cleanup'], + package_dir={'': 'src'}, include_package_data=True, requires=['python (>=3.6)', 'django (>=3.2)'], description='Deletes old files.', diff --git a/django_cleanup/__init__.py b/src/django_cleanup/__init__.py similarity index 100% rename from django_cleanup/__init__.py rename to src/django_cleanup/__init__.py diff --git a/django_cleanup/apps.py b/src/django_cleanup/apps.py similarity index 100% rename from django_cleanup/apps.py rename to src/django_cleanup/apps.py diff --git a/django_cleanup/cache.py b/src/django_cleanup/cache.py similarity index 97% rename from django_cleanup/cache.py rename to src/django_cleanup/cache.py index 692fc2a..cac857c 100644 --- a/django_cleanup/cache.py +++ b/src/django_cleanup/cache.py @@ -131,7 +131,7 @@ def model_has_filefields(model_name): def ignore_model(model, select_mode): '''Check if a model should be ignored''' - return not hasattr(model, get_mangled_select(model)) if select_mode else hasattr(model, get_mangled_ignore(model)) + return (not hasattr(model, get_mangled_select(model))) if select_mode else hasattr(model, get_mangled_ignore(model)) # instance functions ## diff --git a/django_cleanup/cleanup.py b/src/django_cleanup/cleanup.py similarity index 88% rename from django_cleanup/cleanup.py rename to src/django_cleanup/cleanup.py index 86bacb8..eaa17b3 100644 --- a/django_cleanup/cleanup.py +++ b/src/django_cleanup/cleanup.py @@ -1,6 +1,7 @@ '''Public utilities''' from .cache import ( - get_mangled_ignore as _get_mangled_ignore, get_mangled_select as _get_mangled_select, make_cleanup_cache as _make_cleanup_cache) + get_mangled_ignore as _get_mangled_ignore, get_mangled_select as _get_mangled_select, + make_cleanup_cache as _make_cleanup_cache) __all__ = ['refresh', 'cleanup_ignore', 'cleanup_select'] diff --git a/django_cleanup/handlers.py b/src/django_cleanup/handlers.py similarity index 100% rename from django_cleanup/handlers.py rename to src/django_cleanup/handlers.py diff --git a/django_cleanup/signals.py b/src/django_cleanup/signals.py similarity index 100% rename from django_cleanup/signals.py rename to src/django_cleanup/signals.py diff --git a/django_cleanup/testapp/__init__.py b/test/__init__.py similarity index 100% rename from django_cleanup/testapp/__init__.py rename to test/__init__.py diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..9407485 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,49 @@ +import os +import shutil + +from django.conf import settings +from django.db.models.signals import post_delete, post_init, post_save, pre_save + +import pytest + +from django_cleanup import cache, handlers + +from .testing_helpers import get_random_pic_name + + +pytest_plugins = ("test.pytest_plugin",) + + +@pytest.fixture(autouse=True) +def setup_django_cleanup_state(request): + for model in cache.cleanup_models(): + key = '{{}}_django_cleanup_{}'.format(cache.get_model_name(model)) + post_init.disconnect(None, sender=model, + dispatch_uid=key.format('post_init')) + pre_save.disconnect(None, sender=model, + dispatch_uid=key.format('pre_save')) + post_save.disconnect(None, sender=model, + dispatch_uid=key.format('post_save')) + post_delete.disconnect(None, sender=model, + dispatch_uid=key.format('post_delete')) + cache.FIELDS.clear() + selectedConfig = any(m.name == 'CleanupSelectedConfig' for m in request.node.iter_markers()) + + cache.prepare(selectedConfig) + handlers.connect() + + +@pytest.fixture(params=[settings.MEDIA_ROOT]) +def picture(request): + src = os.path.join(request.param, 'pic.jpg') + dst = os.path.join(request.param, get_random_pic_name()) + shutil.copyfile(src, dst) + try: + yield { + 'path': dst, + 'filename': os.path.split(dst)[-1], + 'srcpath': src + } + finally: + if os.path.exists(dst): + os.remove(dst) diff --git a/django_cleanup/testapp/media/pic.jpg b/test/media/pic.jpg similarity index 100% rename from django_cleanup/testapp/media/pic.jpg rename to test/media/pic.jpg diff --git a/django_cleanup/testapp/models/__init__.py b/test/models/__init__.py similarity index 100% rename from django_cleanup/testapp/models/__init__.py rename to test/models/__init__.py diff --git a/django_cleanup/testapp/models/app.py b/test/models/app.py similarity index 77% rename from django_cleanup/testapp/models/app.py rename to test/models/app.py index a21e731..b84ad81 100644 --- a/django_cleanup/testapp/models/app.py +++ b/test/models/app.py @@ -15,17 +15,18 @@ def default_image(): # see the ProductIgnore model below for how to use the decorator @cleanup_ignore class ProductAbstract(models.Model): - image = models.FileField(upload_to='testapp', blank=True, null=True) + image = models.FileField(upload_to='test', blank=True, null=True) image_default = models.FileField( - upload_to='testapp', blank=True, null=True, + upload_to='test', blank=True, null=True, default='pic.jpg') image_default_callable = models.FileField( - upload_to='testapp', blank=True, null=True, default=default_image) + upload_to='test', blank=True, null=True, default=default_image) class Meta: abstract = True +@cleanup.select class Product(ProductAbstract): pass @@ -45,11 +46,11 @@ class ProductUnmanaged(ProductAbstract): class Meta: managed = False - db_table = 'testapp_product' + db_table = 'test_product' class RootProduct(models.Model): pass class BranchProduct(models.Model): root = models.ForeignKey(RootProduct, on_delete=models.CASCADE) - image = models.FileField(upload_to='testapp', blank=True, null=True) + image = models.FileField(upload_to='test', blank=True, null=True) diff --git a/django_cleanup/testapp/models/integration.py b/test/models/integration.py similarity index 74% rename from django_cleanup/testapp/models/integration.py rename to test/models/integration.py index f80e51d..1bff291 100644 --- a/django_cleanup/testapp/models/integration.py +++ b/test/models/integration.py @@ -5,8 +5,8 @@ class ProductIntegrationAbstract(ProductAbstract): - sorl_image = ImageField(upload_to='testapp', blank=True) - easy_image = ThumbnailerImageField(upload_to='testapp', blank=True) + sorl_image = ImageField(upload_to='test', blank=True) + easy_image = ThumbnailerImageField(upload_to='test', blank=True) class Meta: abstract = True diff --git a/test/pytest_plugin/__init__.py b/test/pytest_plugin/__init__.py new file mode 100644 index 0000000..cf7f78e --- /dev/null +++ b/test/pytest_plugin/__init__.py @@ -0,0 +1,9 @@ +import sys + +import pytest + + +@pytest.hookimpl(tryfirst=True) +def pytest_load_initial_conftests(early_config, parser, args): + if hasattr(sys, 'gettrace') and sys.gettrace() is not None and '--forked' in args: + args.remove('--forked') diff --git a/django_cleanup/testapp/requirements.txt b/test/requirements.txt similarity index 87% rename from django_cleanup/testapp/requirements.txt rename to test/requirements.txt index 9a05747..a2dc4d4 100644 --- a/django_cleanup/testapp/requirements.txt +++ b/test/requirements.txt @@ -6,3 +6,4 @@ pytest-django pytest-pythonpath pytest-cov pytest-xdist +pytest-forked diff --git a/django_cleanup/testapp/settings.py b/test/settings.py similarity index 95% rename from django_cleanup/testapp/settings.py rename to test/settings.py index 78ff8ec..9debf63 100644 --- a/django_cleanup/testapp/settings.py +++ b/test/settings.py @@ -13,7 +13,7 @@ } INSTALLED_APPS = ( - 'django_cleanup.testapp', + 'test', 'django_cleanup', ) diff --git a/django_cleanup/testapp/storage.py b/test/storage.py similarity index 100% rename from django_cleanup/testapp/storage.py rename to test/storage.py diff --git a/django_cleanup/testapp/test_all.py b/test/test_all.py similarity index 90% rename from django_cleanup/testapp/test_all.py rename to test/test_all.py index 391bb9a..e1fa762 100644 --- a/django_cleanup/testapp/test_all.py +++ b/test/test_all.py @@ -2,8 +2,8 @@ import os import pickle import re - import tempfile + from django.conf import settings from django.core.files import File from django.db import transaction @@ -16,7 +16,7 @@ from . import storage from .models.app import ( BranchProduct, Product, ProductIgnore, ProductProxy, ProductUnmanaged, RootProduct) -from .testing_helpers import get_random_pic_name, get_using, picture +from .testing_helpers import get_random_pic_name, get_using LINE = re.compile(r'line \d{1,3}') @@ -269,7 +269,7 @@ def test_remove_none(monkeypatch): @pytest.mark.django_db(transaction=True) def test_exception_on_save(settings, picture, caplog): - settings.DEFAULT_FILE_STORAGE = 'django_cleanup.testapp.storage.DeleteErrorStorage' + settings.DEFAULT_FILE_STORAGE = 'test.storage.DeleteErrorStorage' product = Product.objects.create(image=picture['filename']) # simulate a fieldfile that has a storage that raises a filenotfounderror on delete assert os.path.exists(picture['path']) @@ -286,7 +286,7 @@ def test_exception_on_save(settings, picture, caplog): ( 'django_cleanup.handlers', logging.ERROR, - 'There was an exception deleting the file `{}` on field `testapp.product.image`'.format( + 'There was an exception deleting the file `{}` on field `test.product.image`'.format( picture['filename']) ) ] @@ -350,3 +350,35 @@ def test_file_exists_on_create_and_update(): assert os.path.isfile(f1.name) assert os.path.isfile(f2.name) assert not os.path.isfile(product.image.path) + + +#region select config +@pytest.mark.CleanupSelectedConfig +@pytest.mark.django_db(transaction=True) +def test__select_config__replace_file_with_file(picture): + product = Product.objects.create(image=picture['filename']) + assert os.path.exists(picture['path']) + random_pic_name = get_random_pic_name() + product.image = random_pic_name + with transaction.atomic(get_using(product)): + product.save() + assert not os.path.exists(picture['path']) + assert product.image + new_image_path = os.path.join(settings.MEDIA_ROOT, random_pic_name) + assert product.image.path == new_image_path + + +@pytest.mark.CleanupSelectedConfig +@pytest.mark.django_db(transaction=True) +def test__select_config__replace_file_with_file_ignore(picture): + product = ProductIgnore.objects.create(image=picture['filename']) + assert os.path.exists(picture['path']) + random_pic_name = get_random_pic_name() + product.image = random_pic_name + with transaction.atomic(get_using(product)): + product.save() + assert os.path.exists(picture['path']) + assert product.image + new_image_path = os.path.join(settings.MEDIA_ROOT, random_pic_name) + assert product.image.path == new_image_path +#endregion diff --git a/django_cleanup/testapp/test_integration.py b/test/test_integration.py similarity index 90% rename from django_cleanup/testapp/test_integration.py rename to test/test_integration.py index f352170..f693a4f 100644 --- a/django_cleanup/testapp/test_integration.py +++ b/test/test_integration.py @@ -7,13 +7,13 @@ from django_cleanup.signals import cleanup_pre_delete -from .testing_helpers import get_using, picture +from .testing_helpers import get_using @pytest.mark.django_db(transaction=True) def test_sorlthumbnail_replace(settings, picture): # https://github.com/mariocesar/sorl-thumbnail - models = pytest.importorskip("django_cleanup.testapp.models.integration") + models = pytest.importorskip("test.models.integration") ProductIntegration = models.ProductIntegration sorl_delete = models.sorl_delete cleanup_pre_delete.connect(sorl_delete) @@ -35,7 +35,7 @@ def test_sorlthumbnail_replace(settings, picture): @pytest.mark.django_db(transaction=True) def test_sorlthumbnail_delete(picture): # https://github.com/mariocesar/sorl-thumbnail - models = pytest.importorskip("django_cleanup.testapp.models.integration") + models = pytest.importorskip("test.models.integration") ProductIntegration = models.ProductIntegration sorl_delete = models.sorl_delete cleanup_pre_delete.connect(sorl_delete) @@ -56,7 +56,7 @@ def test_sorlthumbnail_delete(picture): @pytest.mark.django_db(transaction=True) def test_easythumbnails_replace(picture): # https://github.com/SmileyChris/easy-thumbnails - models = pytest.importorskip("django_cleanup.testapp.models.integration") + models = pytest.importorskip("test.models.integration") ProductIntegration = models.ProductIntegration from easy_thumbnails.files import get_thumbnailer product = ProductIntegration.objects.create(easy_image=picture['filename']) @@ -75,7 +75,7 @@ def test_easythumbnails_replace(picture): @pytest.mark.django_db(transaction=True) def test_easythumbnails_delete(picture): # https://github.com/SmileyChris/easy-thumbnails - models = pytest.importorskip("django_cleanup.testapp.models.integration") + models = pytest.importorskip("test.models.integration") ProductIntegration = models.ProductIntegration from easy_thumbnails.files import get_thumbnailer product = ProductIntegration.objects.create(easy_image=picture['filename']) diff --git a/test/testing_helpers.py b/test/testing_helpers.py new file mode 100644 index 0000000..e66cfc8 --- /dev/null +++ b/test/testing_helpers.py @@ -0,0 +1,13 @@ +import random +import string + +from django.db import router + + +def get_using(instance): + return router.db_for_write(instance.__class__, instance=instance) + + +def get_random_pic_name(length=20): + return 'pic{}.jpg'.format( + ''.join(random.choice(string.ascii_letters) for m in range(length))) diff --git a/tox.ini b/tox.ini index 7a4dc93..375010b 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ envlist = py{36,37,38,39,310,py3}-django32 py{38,39,310,py3}-django40 - py{38,39,310,311,py3}-django{1, main} + py{38,39,310,311,py3}-django{41, main} [testenv] deps = djangomain: https://github.com/django/django/tarball/main @@ -14,5 +14,5 @@ deps = django40: django<4.1 # LTS April 2021 - April 2024 django32: django<3.3 - -rdjango_cleanup/testapp/requirements.txt -commands=pytest #-k "test_name" + -rtest/requirements.txt +commands=pytest test #-k "test_name" From cfea4ffc02c375720c8cd3c2a23777b1a79ae4fc Mon Sep 17 00:00:00 2001 From: Mario Rosa Date: Sun, 8 Jan 2023 12:07:18 -0500 Subject: [PATCH 07/12] send more info with signals --- src/django_cleanup/handlers.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/django_cleanup/handlers.py b/src/django_cleanup/handlers.py index e40240c..ceb447b 100644 --- a/src/django_cleanup/handlers.py +++ b/src/django_cleanup/handlers.py @@ -46,7 +46,7 @@ def delete_old_post_save(sender, instance, raw, created, update_fields, using, if update_fields is None or field_name in update_fields: old_file = cache.get_field_attr(instance, field_name) if old_file != new_file: - delete_file(instance, field_name, old_file, using) + delete_file(sender, instance, field_name, old_file, using, 'updated') # reset cache cache.make_cleanup_cache(instance) @@ -55,10 +55,10 @@ def delete_old_post_save(sender, instance, raw, created, update_fields, using, def delete_all_post_delete(sender, instance, using, **kwargs): '''Post_delete on all models with file fields, deletes all files''' for field_name, file_ in cache.fields_for_model_instance(instance): - delete_file(instance, field_name, file_, using) + delete_file(sender, instance, field_name, file_, using, 'deleted') -def delete_file(instance, field_name, file_, using): +def delete_file(sender, instance, field_name, file_, using, reason): '''Deletes a file''' if not file_.name: @@ -88,19 +88,29 @@ def delete_file(instance, field_name, file_, using): if not hasattr(file_, 'storage'): file_.storage = cache.get_field_storage(model_name, field_name)() + event = { + 'deleted': reason == 'deleted', + 'field_name': field_name, + 'file': file_, + 'instance': instance, + 'updated': reason == 'updated' + } + # this will run after a successful commit # assuming you are in a transaction and on a database that supports # transactions, otherwise it will run immediately def run_on_commit(): - cleanup_pre_delete.send(sender=None, file=file_) + cleanup_pre_delete.send(sender=sender, **event) + success = False try: file_.delete(save=False) + success = True except Exception: opts = instance._meta logger.exception( 'There was an exception deleting the file `%s` on field `%s.%s.%s`', file_, opts.app_label, opts.model_name, field_name) - cleanup_post_delete.send(sender=None, file=file_) + cleanup_post_delete.send(sender=sender, success=success, **event) on_commit(run_on_commit, using) From 80a35e74455bef628963aac58e18364f0165858f Mon Sep 17 00:00:00 2001 From: Mario Rosa Date: Sun, 8 Jan 2023 12:21:11 -0500 Subject: [PATCH 08/12] add error to post signal --- src/django_cleanup/handlers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/django_cleanup/handlers.py b/src/django_cleanup/handlers.py index ceb447b..6f2e360 100644 --- a/src/django_cleanup/handlers.py +++ b/src/django_cleanup/handlers.py @@ -102,15 +102,17 @@ def delete_file(sender, instance, field_name, file_, using, reason): def run_on_commit(): cleanup_pre_delete.send(sender=sender, **event) success = False + error = None try: file_.delete(save=False) success = True - except Exception: + except Exception as ex: + error = ex opts = instance._meta logger.exception( 'There was an exception deleting the file `%s` on field `%s.%s.%s`', file_, opts.app_label, opts.model_name, field_name) - cleanup_post_delete.send(sender=sender, success=success, **event) + cleanup_post_delete.send(sender=sender, error=error, success=success, **event) on_commit(run_on_commit, using) From eab3ae76438f396c5a485a923e549e566bbdee0d Mon Sep 17 00:00:00 2001 From: Mario Rosa Date: Sun, 8 Jan 2023 12:28:16 -0500 Subject: [PATCH 09/12] update change log --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b98cb5..e09d5d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update to run tests on python 3.11 with django 4.1. - Select mode, with the ability to only cleanup selected models using a `select` decorator. Resolves issue [#75]. +## Changed +- Pass more data to the cleanup_pre_delete and cleanup_post_delete signals. Resolves issue [#96]. + ### Removed - Dropped support for django 2.2 and python 3.5. @@ -114,6 +117,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [0.1.4]: https://github.com/un1t/django-cleanup/compare/0.1.0...0.1.4 [0.1.0]: https://github.com/un1t/django-cleanup/releases/tag/0.1.0 +[#96]: https://github.com/un1t/django-cleanup/issues/96 [#89]: https://github.com/un1t/django-cleanup/issues/89 [#88]: https://github.com/un1t/django-cleanup/pull/88 [#86]: https://github.com/un1t/django-cleanup/pull/86 From a13ae5ea2976a367cc6331a369a6d55f692d6a98 Mon Sep 17 00:00:00 2001 From: Mario Rosa Date: Sat, 28 Jan 2023 17:12:17 -0500 Subject: [PATCH 10/12] add signal tests and update readme --- README.rst | 40 +++++++++++++------ src/django_cleanup/handlers.py | 3 ++ test/models/app.py | 2 + test/test_all.py | 70 +++++++++++++++++++++++++++++++++- tox.ini | 2 +- 5 files changed, 103 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index 8f7d71e..724d088 100644 --- a/README.rst +++ b/README.rst @@ -25,13 +25,7 @@ whether or not a :code:`FileField`'s value has changed a local cache of original the model instance. If a condition is detected that should result in a file deletion, a function to delete the file is setup and inserted into the commit phase of the current transaction. -**Warning! If you are using a database that does not support transactions you may lose files if a -transaction will rollback at the right instance. This outcome is mitigated by our use of -post_save and post_delete signals, and by following the recommended configuration below. This -outcome will still occur if there are signals registered after app initialization and there are -exceptions when those signals are handled. In this case, the old file will be lost and the new file -will not be referenced in a model, though the new file will likely still exist on disk. If you are -concerned about this behavior you will need another solution for old file deletion in your project.** +**Warning! Please be aware of the Known Limitations documented below!** Installation ============ @@ -77,6 +71,27 @@ You can check if your ``Model`` is loaded by using from django.apps import apps apps.get_models() +Known Limitations +================= + +Database Should Support Transactions +------------------------------------ +If you are using a database that does not support transactions you may lose files if a +transaction will rollback at the right instance. This outcome is mitigated by our use of +post_save and post_delete signals, and by following the recommended configuration in this README. +This outcome will still occur if there are signals registered after app initialization and there are +exceptions when those signals are handled. In this case, the old file will be lost and the new file +will not be referenced in a model, though the new file will likely still exist on disk. If you are +concerned about this behavior you will need another solution for old file deletion in your project. + +File referenced by multiple model instances +------------------------------------------- +This app is designed with the assumption that each file is referenced only once. If you are sharing +a file over two or more model instances you will not have the desired functionality. If you want to +reference a file from multiple models add a level of indirection. That is, use a separate file model +that is referenced from other models through a foreign key. There are many file management apps +already available in the django ecosystem that fulfill this behavior. + Advanced ======== This section contains additional functionality that can be used to interact with django-cleanup for @@ -115,7 +130,8 @@ There have been rare cases where the cache would need to be refreshed. To do so Ignore cleanup for a specific model ----------------------------------- -Ignore a model and do not perform cleanup when the model is deleted or its files change. +To ignore a model and not have cleanup performed when the model is deleted or its files change, use +the :code:`ignore` decorator to mark that model: .. code-block:: py @@ -128,10 +144,10 @@ Ignore a model and do not perform cleanup when the model is deleted or its files Only cleanup selected models ---------------------------- If you have many models to ignore, or if you prefer to be explicit about what models are selected, -you can change the mode of django-cleanup to select mode by using the select mode app config. In -your ``INSTALLED_APPS`` setting you will replace ``'django_cleanup.apps.CleanupConfig'`` -with ``'django_cleanup.apps.CleanupSelectedConfig'``. Then use the ``select`` decorator to mark a -model for cleanup: +you can change the mode of django-cleanup to "select mode" by using the select mode app config. In +your ``INSTALLED_APPS`` setting you will replace ':code:`django_cleanup.apps.CleanupConfig`' +with ':code:`django_cleanup.apps.CleanupSelectedConfig`'. Then use the :code:`select` decorator to +mark a model for cleanup: .. code-block:: py diff --git a/src/django_cleanup/handlers.py b/src/django_cleanup/handlers.py index 6f2e360..aa6a8c5 100644 --- a/src/django_cleanup/handlers.py +++ b/src/django_cleanup/handlers.py @@ -90,7 +90,10 @@ def delete_file(sender, instance, field_name, file_, using, reason): event = { 'deleted': reason == 'deleted', + 'model_name': model_name, 'field_name': field_name, + 'file_name': file_.name, + 'default_file_name': default, 'file': file_, 'instance': instance, 'updated': reason == 'updated' diff --git a/test/models/app.py b/test/models/app.py index b84ad81..5a50bf4 100644 --- a/test/models/app.py +++ b/test/models/app.py @@ -48,9 +48,11 @@ class Meta: managed = False db_table = 'test_product' + class RootProduct(models.Model): pass + class BranchProduct(models.Model): root = models.ForeignKey(RootProduct, on_delete=models.CASCADE) image = models.FileField(upload_to='test', blank=True, null=True) diff --git a/test/test_all.py b/test/test_all.py index e1fa762..1f1692a 100644 --- a/test/test_all.py +++ b/test/test_all.py @@ -7,11 +7,12 @@ from django.conf import settings from django.core.files import File from django.db import transaction -from django.db.models.fields import files +from django.db.models.fields import NOT_PROVIDED, files import pytest from django_cleanup import cache, handlers +from django_cleanup.signals import cleanup_post_delete, cleanup_pre_delete from . import storage from .models.app import ( @@ -352,6 +353,73 @@ def test_file_exists_on_create_and_update(): assert not os.path.isfile(product.image.path) +@pytest.mark.django_db(transaction=True) +def test_signals(picture): + prekwargs = None + postkwargs = None + def assn_prekwargs(**kwargs): + nonlocal prekwargs + prekwargs = kwargs + + def assn_postkwargs(**kwargs): + nonlocal postkwargs + postkwargs = kwargs + + cleanup_pre_delete.connect(assn_prekwargs, dispatch_uid='pre_test_replace_file_with_file_signals') + cleanup_post_delete.connect(assn_postkwargs, dispatch_uid='post_test_replace_file_with_file_signals') + product = Product.objects.create(image=picture['filename']) + random_pic_name = get_random_pic_name() + product.image = random_pic_name + with transaction.atomic(get_using(product)): + product.save() + + assert prekwargs['deleted'] == False + assert prekwargs['updated'] == True + assert prekwargs['instance'] == product + assert prekwargs['file'] is not None + assert prekwargs['file_name'] == picture['filename'] + assert isinstance(prekwargs['default_file_name'], NOT_PROVIDED) + assert prekwargs['model_name'] == 'test.product' + assert prekwargs['field_name'] == 'image' + + assert postkwargs['deleted'] == False + assert postkwargs['updated'] == True + assert postkwargs['instance'] == product + assert postkwargs['file'] is not None + assert postkwargs['file_name'] == picture['filename'] + assert isinstance(postkwargs['default_file_name'], NOT_PROVIDED) + assert postkwargs['model_name'] == 'test.product' + assert postkwargs['field_name'] == 'image' + assert postkwargs['success'] == True + assert postkwargs['error'] is None + + with transaction.atomic(get_using(product)): + product.delete() + + assert prekwargs['deleted'] == True + assert prekwargs['updated'] == False + assert prekwargs['instance'] == product + assert prekwargs['file'] is not None + assert prekwargs['file_name'] == random_pic_name + assert isinstance(prekwargs['default_file_name'], NOT_PROVIDED) + assert prekwargs['model_name'] == 'test.product' + assert prekwargs['field_name'] == 'image' + + assert postkwargs['deleted'] == True + assert postkwargs['updated'] == False + assert postkwargs['instance'] == product + assert postkwargs['file'] is not None + assert postkwargs['file_name'] == random_pic_name + assert isinstance(postkwargs['default_file_name'], NOT_PROVIDED) + assert postkwargs['model_name'] == 'test.product' + assert postkwargs['field_name'] == 'image' + assert postkwargs['success'] == True + assert postkwargs['error'] is None + + cleanup_pre_delete.disconnect(None, dispatch_uid='pre_test_replace_file_with_file_signals') + cleanup_post_delete.disconnect(None, dispatch_uid='post_test_replace_file_with_file_signals') + + #region select config @pytest.mark.CleanupSelectedConfig @pytest.mark.django_db(transaction=True) diff --git a/tox.ini b/tox.ini index 375010b..efa353f 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ envlist = py{36,37,38,39,310,py3}-django32 py{38,39,310,py3}-django40 - py{38,39,310,311,py3}-django{41, main} + py{38,39,310,311,py3}-django{41,main} [testenv] deps = djangomain: https://github.com/django/django/tarball/main From 31816fe0f2ef8c212ddbf436f089c1d78a0a1f67 Mon Sep 17 00:00:00 2001 From: Mario Rosa Date: Sat, 11 Feb 2023 10:31:34 -0500 Subject: [PATCH 11/12] some final changes --- .github/workflows/main.yml | 2 +- CHANGELOG.md | 13 ++++++++----- README.rst | 15 ++++++++------- src/django_cleanup/apps.py | 2 +- src/django_cleanup/cache.py | 3 ++- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0e07550..bc9a569 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,7 +42,7 @@ jobs: - python: "3.10" toxenv: py310-django41 - python: "3.11" - toxenv: py310-django41 + toxenv: py311-django41 - python: "pypy-3.8" toxenv: pypy3-django41 steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index e09d5d8..0a63e73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ## [7.0.0] - TBD ### Added -- Update to run tests for django 4.1. -- Update to run tests on python 3.11 with django 4.1. -- Select mode, with the ability to only cleanup selected models using a `select` decorator. Resolves issue [#75]. +- Run tests for django 4.1. +- Run tests on python 3.11 with django 4.1. +- Select mode, with the ability to only cleanup selected models using a `select` decorator. Resolves issue [#75] for [@daviddavis](https://github.com/daviddavis). +- Documentation on the known limitations of referencing a file by multiple model instances. Resolves issue [#98] for [@Grosskopf](https://github.com/Grosskopf) ## Changed -- Pass more data to the cleanup_pre_delete and cleanup_post_delete signals. Resolves issue [#96]. +- Pass more data to the cleanup_pre_delete and cleanup_post_delete signals. Resolves issue [#96] for [@NadavK](https://github.com/NadavK). ### Removed - Dropped support for django 2.2 and python 3.5. @@ -81,7 +82,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.4] - 2012-08-16 ## [0.1.0] - 2012-08-14 -[Unreleased]: https://github.com/un1t/django-cleanup/compare/6.0.0...HEAD +[Unreleased]: https://github.com/un1t/django-cleanup/compare/7.0.0...HEAD [7.0.0]: https://github.com/un1t/django-cleanup/compare/6.0.0...7.0.0 [6.0.0]: https://github.com/un1t/django-cleanup/compare/5.2.0...6.0.0 [5.2.0]: https://github.com/un1t/django-cleanup/compare/5.1.0...5.2.0 @@ -117,6 +118,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [0.1.4]: https://github.com/un1t/django-cleanup/compare/0.1.0...0.1.4 [0.1.0]: https://github.com/un1t/django-cleanup/releases/tag/0.1.0 +[#98]: https://github.com/un1t/django-cleanup/issues/98 [#96]: https://github.com/un1t/django-cleanup/issues/96 [#89]: https://github.com/un1t/django-cleanup/issues/89 [#88]: https://github.com/un1t/django-cleanup/pull/88 @@ -124,5 +126,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#81]: https://github.com/un1t/django-cleanup/pull/81 [#80]: https://github.com/un1t/django-cleanup/pull/80 [#76]: https://github.com/un1t/django-cleanup/pull/76 +[#75]: https://github.com/un1t/django-cleanup/issues/75 [#74]: https://github.com/un1t/django-cleanup/pull/74 [#73]: https://github.com/un1t/django-cleanup/issues/73 diff --git a/README.rst b/README.rst index 724d088..6fea595 100644 --- a/README.rst +++ b/README.rst @@ -25,7 +25,7 @@ whether or not a :code:`FileField`'s value has changed a local cache of original the model instance. If a condition is detected that should result in a file deletion, a function to delete the file is setup and inserted into the commit phase of the current transaction. -**Warning! Please be aware of the Known Limitations documented below!** +**Warning! Please be aware of the known limitations documented below!** Installation ============ @@ -71,10 +71,10 @@ You can check if your ``Model`` is loaded by using from django.apps import apps apps.get_models() -Known Limitations +Known limitations ================= -Database Should Support Transactions +Database should support transactions ------------------------------------ If you are using a database that does not support transactions you may lose files if a transaction will rollback at the right instance. This outcome is mitigated by our use of @@ -87,10 +87,11 @@ concerned about this behavior you will need another solution for old file deleti File referenced by multiple model instances ------------------------------------------- This app is designed with the assumption that each file is referenced only once. If you are sharing -a file over two or more model instances you will not have the desired functionality. If you want to -reference a file from multiple models add a level of indirection. That is, use a separate file model -that is referenced from other models through a foreign key. There are many file management apps -already available in the django ecosystem that fulfill this behavior. +a file over two or more model instances you will not have the desired functionality. Be cautious of +copying model instances, as this will cause a file to be shared by more than one instance. If you +want to reference a file from multiple models add a level of indirection. That is, use a separate +file model that is referenced from other models through a foreign key. There are many file +management apps already available in the django ecosystem that fulfill this behavior. Advanced ======== diff --git a/src/django_cleanup/apps.py b/src/django_cleanup/apps.py index 1a8a709..002965f 100644 --- a/src/django_cleanup/apps.py +++ b/src/django_cleanup/apps.py @@ -12,7 +12,7 @@ class CleanupConfig(AppConfig): default = True def ready(self): - cache.prepare() + cache.prepare(False) handlers.connect() class CleanupSelectedConfig(AppConfig): diff --git a/src/django_cleanup/cache.py b/src/django_cleanup/cache.py index cac857c..a2e3423 100644 --- a/src/django_cleanup/cache.py +++ b/src/django_cleanup/cache.py @@ -24,7 +24,7 @@ def fields_dict_default(): # cache init ## -def prepare(select_mode=False): +def prepare(select_mode): '''Prepare the cache for all models, non-reentrant''' if FIELDS: # pragma: no cover return @@ -121,6 +121,7 @@ def get_mangled_select(model): '''returns a mangled attribute name specific to the model for select functionality''' return '_{opt.model_name}__{opt.app_label}_cleanup_select'.format(opt=model._meta) + # booleans ## From f40c07c05d933a963301c9617b043a2db26bef4e Mon Sep 17 00:00:00 2001 From: Mario Rosa Date: Sat, 11 Feb 2023 11:23:44 -0500 Subject: [PATCH 12/12] updates for release --- CHANGELOG.md | 2 +- src/django_cleanup/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a63e73..59b6de9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -## [7.0.0] - TBD +## [7.0.0] - 2023-02-11 ### Added - Run tests for django 4.1. - Run tests on python 3.11 with django 4.1. diff --git a/src/django_cleanup/__init__.py b/src/django_cleanup/__init__.py index a5b349f..89b31a9 100644 --- a/src/django_cleanup/__init__.py +++ b/src/django_cleanup/__init__.py @@ -4,4 +4,4 @@ will delete files on model instance deletion. ''' -__version__ = '7.0.0-dev0001' +__version__ = '7.0.0'