New Upstream Release - django-cleanup
Ready changes
Summary
Merged new upstream version: 7.0.0 (was: 6.0.0).
Resulting package
Built on 2023-05-26T21:35 (took 4m31s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-releases python3-django-cleanup
Lintian Result
Diff
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd64ac7..59b6de9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,30 @@ 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] - 2023-02-11
+### Added
+- 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] for [@NadavK](https://github.com/NadavK).
+
+### 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).
+- GitHub Actions. Resolves issue [#89] for [@johnthagen](https://github.com/johnthagen).
+
+### Changed
+- Fix default_app_config deprecation. PR [#86] from [@nikolaik](https://github.com/nikolaik).
+
+### Removed
+- Dropped support for django 3.0 and 3.1.
+- Travis configuration.
## [5.2.0] - 2021-04-18
### Added
@@ -58,7 +82,9 @@ 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/5.1.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
[5.1.0]: https://github.com/un1t/django-cleanup/compare/5.0.0...5.1.0
[5.0.0]: https://github.com/un1t/django-cleanup/compare/4.0.1...5.0.0
@@ -92,8 +118,14 @@ 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
+[#86]: https://github.com/un1t/django-cleanup/pull/86
[#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/PKG-INFO b/PKG-INFO
index 7f158d8..98bf080 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,225 +1,258 @@
Metadata-Version: 2.1
Name: django-cleanup
-Version: 5.2.0
+Version: 7.0.0
Summary: Deletes old files.
Home-page: https://github.com/un1t/django-cleanup
+Download-URL: https://github.com/un1t/django-cleanup/tarball/master
Author: Ilya Shalyapin
Author-email: ishalyapin@gmail.com
License: MIT License
-Download-URL: https://github.com/un1t/django-cleanup/tarball/master
-Description: Django Cleanup
- **************
- |Version| |Status| |License|
-
- Features
- ========
- The django-cleanup app automatically deletes files for :code:`FileField`, :code:`ImageField` and
- subclasses. When a :code:`FileField`'s value is changed and the model is saved, the old file is
- deleted. When a model that has a :code:`FileField` is deleted, the file is also deleted. A file that
- is set as the :code:`FileField`'s default value will not be deleted.
-
- Compatibility
- -------------
- - Django 2.2, 3.0, 3.1, 3.2 (`See Django Supported Versions <https://www.djangoproject.com/download/#supported-versions>`_)
- - Python 3.5+
- - Compatible with `sorl-thumbnail <https://github.com/jazzband/sorl-thumbnail>`_
- - Compatible with `easy-thumbnail <https://github.com/SmileyChris/easy-thumbnails>`_
-
- How does it work?
- =================
- In order to track changes of a :code:`FileField` and facilitate file deletions, django-cleanup
- connects :code:`post_init`, :code:`pre_save`, :code:`post_save` and :code:`post_delete` signals to
- signal handlers for each :code:`INSTALLED_APPS` model that has a :code:`FileField`. In order to tell
- whether or not a :code:`FileField`'s value has changed a local cache of original values is kept on
- 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.**
-
- Installation
- ============
- ::
-
- pip install django-cleanup
-
-
- Configuration
- =============
- Add ``django_cleanup`` to the bottom of ``INSTALLED_APPS`` in ``settings.py``
-
- .. code-block:: py
-
- INSTALLED_APPS = (
- ...,
- 'django_cleanup.apps.CleanupConfig',
- )
-
- That is all, no other configuration is necessary.
-
- Note: Order of ``INSTALLED_APPS`` is important. To ensure that exceptions inside other apps' signal
- handlers do not affect the integrity of file deletions within transactions, ``django_cleanup``
- should be placed last in ``INSTALLED_APPS``.
-
- Troubleshooting
- ===============
- If you notice that ``django-cleanup`` is not removing files when expected, check that your models
- are being properly
- `loaded <https://docs.djangoproject.com/en/stable/ref/applications/#how-applications-are-loaded>`_:
-
- You must define or import all models in your application's models.py or models/__init__.py.
- Otherwise, the application registry may not be fully populated at this point, which could cause
- the ORM to malfunction.
-
- If your models are not loaded, ``django-cleanup`` will not be able to discover their
- ``FileField``'s.
-
- You can check if your ``Model`` is loaded by using
-
- .. code-block:: py
-
- from django.apps import apps
- apps.get_models()
-
- Advanced
- ========
- This section contains additional functionality that can be used to interact with django-cleanup for
- special cases.
-
- Signals
- -------
- To facilitate interactions with other django apps django-cleanup sends the following signals which
- can be imported from :code:`django_cleanup.signals`:
-
- - :code:`cleanup_pre_delete`: just before a file is deleted. Passes a :code:`file` keyword argument.
- - :code:`cleanup_post_delete`: just after a file is deleted. Passes a :code:`file` keyword argument.
-
- Signals example for sorl.thumbnail:
-
- .. code-block:: py
-
- from django_cleanup.signals import cleanup_pre_delete
- from sorl.thumbnail import delete
-
- def sorl_delete(**kwargs):
- delete(kwargs['file'])
-
- cleanup_pre_delete.connect(sorl_delete)
-
- Refresh the cache
- -----------------
- There have been rare cases where the cache would need to be refreshed. To do so the
- :code:`django_cleanup.cleanup.refresh` method can be used:
-
- .. code-block:: py
-
- from django_cleanup import cleanup
-
- cleanup.refresh(model_instance)
-
- Ignore cleanup for a specific model
- -----------------------------------
- Ignore a model and do not perform cleanup when the model is deleted or its files change.
-
- .. code-block:: py
-
- from django_cleanup import cleanup
-
- @cleanup.ignore
- class MyModel(models.Model):
- image = models.FileField()
-
- How to run tests
- ================
- Install, setup and use pyenv_ to install all the required versions of cPython
- (see the `tox.ini <https://github.com/un1t/django-cleanup/blob/master/tox.ini>`_).
-
- Setup pyenv_ to have all versions of python activated within your local django-cleanup repository.
- Ensuring that the python 3.8 that was installed is first priority.
-
- Install tox_ on python 3.8 and run the :code:`tox` command from your local django-cleanup
- repository.
-
- How to write tests
- ==================
- This app requires the use of django.test.TransactionTestCase_ when writing tests.
-
- For details on why this is required see `here
- <https://docs.djangoproject.com/en/stable/topics/db/transactions/#use-in-tests>`_:
-
- Django's :code:`TestCase` class wraps each test in a transaction and rolls back that transaction
- after each test, in order to provide test isolation. This means that no transaction is ever
- actually committed, thus your :code:`on_commit()` callbacks will never be run. If you need to
- test the results of an :code:`on_commit()` callback, use a :code:`TransactionTestCase` instead.
-
- License
- =======
- django-cleanup is free software under terms of the:
-
- MIT License
-
- Copyright (C) 2012 by Ilya Shalyapin, ishalyapin@gmail.com
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
-
-
- .. _django.test.TransactionTestCase: https://docs.djangoproject.com/en/stable/topics/testing/tools/#django.test.TransactionTestCase
- .. _pyenv: https://github.com/pyenv/pyenv
- .. _tox: https://tox.readthedocs.io/en/latest/
-
- .. |Version| image:: https://img.shields.io/pypi/v/django-cleanup.svg
- :target: https://pypi.python.org/pypi/django-cleanup/
- :alt: PyPI Package
- .. |Status| image:: https://travis-ci.org/un1t/django-cleanup.svg?branch=master
- :target: https://travis-ci.org/un1t/django-cleanup
- :alt: Build Status
- .. |License| image:: https://img.shields.io/badge/license-MIT-maroon
- :target: https://github.com/un1t/django-cleanup/blob/master/LICENSE
- :alt: MIT License
-
Keywords: django
-Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
-Classifier: Framework :: Django :: 2.2
-Classifier: Framework :: Django :: 3.0
-Classifier: Framework :: Django :: 3.1
Classifier: Framework :: Django :: 3.2
+Classifier: Framework :: Django :: 4.0
+Classifier: Framework :: Django :: 4.1
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Utilities
-Requires: python (>=3.5)
-Requires: django (>=2.2)
+Requires: python (>=3.6)
+Requires: django (>=3.2)
Description-Content-Type: text/x-rst
+License-File: LICENSE
+
+Django Cleanup
+**************
+|Version| |Status| |License|
+
+Features
+========
+The django-cleanup app automatically deletes files for :code:`FileField`, :code:`ImageField` and
+subclasses. When a :code:`FileField`'s value is changed and the model is saved, the old file is
+deleted. When a model that has a :code:`FileField` is deleted, the file is also deleted. A file that
+is set as the :code:`FileField`'s default value will not be deleted.
+
+Compatibility
+-------------
+- Django 3.2, 4.0, 4.1 (`See Django Supported Versions <https://www.djangoproject.com/download/#supported-versions>`_)
+- Python 3.6+
+- Compatible with `sorl-thumbnail <https://github.com/jazzband/sorl-thumbnail>`_
+- Compatible with `easy-thumbnail <https://github.com/SmileyChris/easy-thumbnails>`_
+
+How does it work?
+=================
+In order to track changes of a :code:`FileField` and facilitate file deletions, django-cleanup
+connects :code:`post_init`, :code:`pre_save`, :code:`post_save` and :code:`post_delete` signals to
+signal handlers for each :code:`INSTALLED_APPS` model that has a :code:`FileField`. In order to tell
+whether or not a :code:`FileField`'s value has changed a local cache of original values is kept on
+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!**
+
+Installation
+============
+::
+
+ pip install django-cleanup
+
+
+Configuration
+=============
+Add ``django_cleanup`` to the bottom of ``INSTALLED_APPS`` in ``settings.py``
+
+.. code-block:: py
+
+ INSTALLED_APPS = (
+ ...,
+ 'django_cleanup.apps.CleanupConfig',
+ )
+
+That is all, no other configuration is necessary.
+
+Note: Order of ``INSTALLED_APPS`` is important. To ensure that exceptions inside other apps' signal
+handlers do not affect the integrity of file deletions within transactions, ``django_cleanup``
+should be placed last in ``INSTALLED_APPS``.
+
+Troubleshooting
+===============
+If you notice that ``django-cleanup`` is not removing files when expected, check that your models
+are being properly
+`loaded <https://docs.djangoproject.com/en/stable/ref/applications/#how-applications-are-loaded>`_:
+
+ You must define or import all models in your application's models.py or models/__init__.py.
+ Otherwise, the application registry may not be fully populated at this point, which could cause
+ the ORM to malfunction.
+
+If your models are not loaded, ``django-cleanup`` will not be able to discover their
+``FileField``'s.
+
+You can check if your ``Model`` is loaded by using
+
+.. code-block:: py
+
+ 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. 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
+========
+This section contains additional functionality that can be used to interact with django-cleanup for
+special cases.
+
+Signals
+-------
+To facilitate interactions with other django apps django-cleanup sends the following signals which
+can be imported from :code:`django_cleanup.signals`:
+
+- :code:`cleanup_pre_delete`: just before a file is deleted. Passes a :code:`file` keyword argument.
+- :code:`cleanup_post_delete`: just after a file is deleted. Passes a :code:`file` keyword argument.
+
+Signals example for sorl.thumbnail:
+
+.. code-block:: py
+
+ from django_cleanup.signals import cleanup_pre_delete
+ from sorl.thumbnail import delete
+
+ def sorl_delete(**kwargs):
+ delete(kwargs['file'])
+
+ cleanup_pre_delete.connect(sorl_delete)
+
+Refresh the cache
+-----------------
+There have been rare cases where the cache would need to be refreshed. To do so the
+:code:`django_cleanup.cleanup.refresh` method can be used:
+
+.. code-block:: py
+
+ from django_cleanup import cleanup
+
+ cleanup.refresh(model_instance)
+
+Ignore cleanup for a specific model
+-----------------------------------
+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
+
+ from django_cleanup import cleanup
+
+ @cleanup.ignore
+ 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 ':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
+
+ 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
+(see the `tox.ini <https://github.com/un1t/django-cleanup/blob/master/tox.ini>`_).
+
+Setup pyenv_ to have all versions of python activated within your local django-cleanup repository.
+Ensuring that the python 3.10 that was installed is first priority.
+
+Install tox_ on python 3.10 and run the :code:`tox` command from your local django-cleanup
+repository.
+
+How to write tests
+==================
+This app requires the use of django.test.TransactionTestCase_ when writing tests.
+
+For details on why this is required see `here
+<https://docs.djangoproject.com/en/stable/topics/db/transactions/#use-in-tests>`_:
+
+ Django's :code:`TestCase` class wraps each test in a transaction and rolls back that transaction
+ after each test, in order to provide test isolation. This means that no transaction is ever
+ actually committed, thus your :code:`on_commit()` callbacks will never be run. If you need to
+ test the results of an :code:`on_commit()` callback, use a :code:`TransactionTestCase` instead.
+
+License
+=======
+django-cleanup is free software under terms of the:
+
+MIT License
+
+Copyright (C) 2012 by Ilya Shalyapin, ishalyapin@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+.. _django.test.TransactionTestCase: https://docs.djangoproject.com/en/stable/topics/testing/tools/#django.test.TransactionTestCase
+.. _pyenv: https://github.com/pyenv/pyenv
+.. _tox: https://tox.readthedocs.io/en/latest/
+
+.. |Version| image:: https://img.shields.io/pypi/v/django-cleanup.svg
+ :target: https://pypi.python.org/pypi/django-cleanup/
+ :alt: PyPI Package
+.. |Status| image:: https://github.com/un1t/django-cleanup/actions/workflows/main.yml/badge.svg
+ :target: https://github.com/un1t/django-cleanup/actions/workflows/main.yml
+ :alt: Build Status
+.. |License| image:: https://img.shields.io/badge/license-MIT-maroon
+ :target: https://github.com/un1t/django-cleanup/blob/master/LICENSE
+ :alt: MIT License
diff --git a/README.rst b/README.rst
index 6314345..6fea595 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.0, 3.1, 3.2 (`See Django Supported Versions <https://www.djangoproject.com/download/#supported-versions>`_)
-- Python 3.5+
+- Django 3.2, 4.0, 4.1 (`See Django Supported Versions <https://www.djangoproject.com/download/#supported-versions>`_)
+- Python 3.6+
- Compatible with `sorl-thumbnail <https://github.com/jazzband/sorl-thumbnail>`_
- Compatible with `easy-thumbnail <https://github.com/SmileyChris/easy-thumbnails>`_
@@ -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,28 @@ 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. 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
========
This section contains additional functionality that can be used to interact with django-cleanup for
@@ -115,7 +131,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
@@ -125,15 +142,31 @@ 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 ':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
+
+ 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
(see the `tox.ini <https://github.com/un1t/django-cleanup/blob/master/tox.ini>`_).
Setup pyenv_ to have all versions of python activated within your local django-cleanup repository.
-Ensuring that the python 3.8 that was installed is first priority.
+Ensuring that the python 3.10 that was installed is first priority.
-Install tox_ on python 3.8 and run the :code:`tox` command from your local django-cleanup
+Install tox_ on python 3.10 and run the :code:`tox` command from your local django-cleanup
repository.
How to write tests
@@ -182,8 +215,8 @@ SOFTWARE.
.. |Version| image:: https://img.shields.io/pypi/v/django-cleanup.svg
:target: https://pypi.python.org/pypi/django-cleanup/
:alt: PyPI Package
-.. |Status| image:: https://travis-ci.org/un1t/django-cleanup.svg?branch=master
- :target: https://travis-ci.org/un1t/django-cleanup
+.. |Status| image:: https://github.com/un1t/django-cleanup/actions/workflows/main.yml/badge.svg
+ :target: https://github.com/un1t/django-cleanup/actions/workflows/main.yml
:alt: Build Status
.. |License| image:: https://img.shields.io/badge/license-MIT-maroon
:target: https://github.com/un1t/django-cleanup/blob/master/LICENSE
diff --git a/debian/changelog b/debian/changelog
index 3f88bae..bfff161 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,8 +1,10 @@
-django-cleanup (5.2.0-3) UNRELEASED; urgency=medium
+django-cleanup (7.0.0-1) UNRELEASED; urgency=medium
* Update standards version to 4.6.1, no changes needed.
+ * New upstream release.
+ * New upstream release.
- -- Debian Janitor <janitor@jelmer.uk> Fri, 28 Oct 2022 06:54:45 -0000
+ -- Debian Janitor <janitor@jelmer.uk> Fri, 26 May 2023 21:31:58 -0000
django-cleanup (5.2.0-2) unstable; urgency=medium
diff --git a/django_cleanup.egg-info/PKG-INFO b/django_cleanup.egg-info/PKG-INFO
deleted file mode 100644
index 7f158d8..0000000
--- a/django_cleanup.egg-info/PKG-INFO
+++ /dev/null
@@ -1,225 +0,0 @@
-Metadata-Version: 2.1
-Name: django-cleanup
-Version: 5.2.0
-Summary: Deletes old files.
-Home-page: https://github.com/un1t/django-cleanup
-Author: Ilya Shalyapin
-Author-email: ishalyapin@gmail.com
-License: MIT License
-Download-URL: https://github.com/un1t/django-cleanup/tarball/master
-Description: Django Cleanup
- **************
- |Version| |Status| |License|
-
- Features
- ========
- The django-cleanup app automatically deletes files for :code:`FileField`, :code:`ImageField` and
- subclasses. When a :code:`FileField`'s value is changed and the model is saved, the old file is
- deleted. When a model that has a :code:`FileField` is deleted, the file is also deleted. A file that
- is set as the :code:`FileField`'s default value will not be deleted.
-
- Compatibility
- -------------
- - Django 2.2, 3.0, 3.1, 3.2 (`See Django Supported Versions <https://www.djangoproject.com/download/#supported-versions>`_)
- - Python 3.5+
- - Compatible with `sorl-thumbnail <https://github.com/jazzband/sorl-thumbnail>`_
- - Compatible with `easy-thumbnail <https://github.com/SmileyChris/easy-thumbnails>`_
-
- How does it work?
- =================
- In order to track changes of a :code:`FileField` and facilitate file deletions, django-cleanup
- connects :code:`post_init`, :code:`pre_save`, :code:`post_save` and :code:`post_delete` signals to
- signal handlers for each :code:`INSTALLED_APPS` model that has a :code:`FileField`. In order to tell
- whether or not a :code:`FileField`'s value has changed a local cache of original values is kept on
- 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.**
-
- Installation
- ============
- ::
-
- pip install django-cleanup
-
-
- Configuration
- =============
- Add ``django_cleanup`` to the bottom of ``INSTALLED_APPS`` in ``settings.py``
-
- .. code-block:: py
-
- INSTALLED_APPS = (
- ...,
- 'django_cleanup.apps.CleanupConfig',
- )
-
- That is all, no other configuration is necessary.
-
- Note: Order of ``INSTALLED_APPS`` is important. To ensure that exceptions inside other apps' signal
- handlers do not affect the integrity of file deletions within transactions, ``django_cleanup``
- should be placed last in ``INSTALLED_APPS``.
-
- Troubleshooting
- ===============
- If you notice that ``django-cleanup`` is not removing files when expected, check that your models
- are being properly
- `loaded <https://docs.djangoproject.com/en/stable/ref/applications/#how-applications-are-loaded>`_:
-
- You must define or import all models in your application's models.py or models/__init__.py.
- Otherwise, the application registry may not be fully populated at this point, which could cause
- the ORM to malfunction.
-
- If your models are not loaded, ``django-cleanup`` will not be able to discover their
- ``FileField``'s.
-
- You can check if your ``Model`` is loaded by using
-
- .. code-block:: py
-
- from django.apps import apps
- apps.get_models()
-
- Advanced
- ========
- This section contains additional functionality that can be used to interact with django-cleanup for
- special cases.
-
- Signals
- -------
- To facilitate interactions with other django apps django-cleanup sends the following signals which
- can be imported from :code:`django_cleanup.signals`:
-
- - :code:`cleanup_pre_delete`: just before a file is deleted. Passes a :code:`file` keyword argument.
- - :code:`cleanup_post_delete`: just after a file is deleted. Passes a :code:`file` keyword argument.
-
- Signals example for sorl.thumbnail:
-
- .. code-block:: py
-
- from django_cleanup.signals import cleanup_pre_delete
- from sorl.thumbnail import delete
-
- def sorl_delete(**kwargs):
- delete(kwargs['file'])
-
- cleanup_pre_delete.connect(sorl_delete)
-
- Refresh the cache
- -----------------
- There have been rare cases where the cache would need to be refreshed. To do so the
- :code:`django_cleanup.cleanup.refresh` method can be used:
-
- .. code-block:: py
-
- from django_cleanup import cleanup
-
- cleanup.refresh(model_instance)
-
- Ignore cleanup for a specific model
- -----------------------------------
- Ignore a model and do not perform cleanup when the model is deleted or its files change.
-
- .. code-block:: py
-
- from django_cleanup import cleanup
-
- @cleanup.ignore
- class MyModel(models.Model):
- image = models.FileField()
-
- How to run tests
- ================
- Install, setup and use pyenv_ to install all the required versions of cPython
- (see the `tox.ini <https://github.com/un1t/django-cleanup/blob/master/tox.ini>`_).
-
- Setup pyenv_ to have all versions of python activated within your local django-cleanup repository.
- Ensuring that the python 3.8 that was installed is first priority.
-
- Install tox_ on python 3.8 and run the :code:`tox` command from your local django-cleanup
- repository.
-
- How to write tests
- ==================
- This app requires the use of django.test.TransactionTestCase_ when writing tests.
-
- For details on why this is required see `here
- <https://docs.djangoproject.com/en/stable/topics/db/transactions/#use-in-tests>`_:
-
- Django's :code:`TestCase` class wraps each test in a transaction and rolls back that transaction
- after each test, in order to provide test isolation. This means that no transaction is ever
- actually committed, thus your :code:`on_commit()` callbacks will never be run. If you need to
- test the results of an :code:`on_commit()` callback, use a :code:`TransactionTestCase` instead.
-
- License
- =======
- django-cleanup is free software under terms of the:
-
- MIT License
-
- Copyright (C) 2012 by Ilya Shalyapin, ishalyapin@gmail.com
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
-
-
- .. _django.test.TransactionTestCase: https://docs.djangoproject.com/en/stable/topics/testing/tools/#django.test.TransactionTestCase
- .. _pyenv: https://github.com/pyenv/pyenv
- .. _tox: https://tox.readthedocs.io/en/latest/
-
- .. |Version| image:: https://img.shields.io/pypi/v/django-cleanup.svg
- :target: https://pypi.python.org/pypi/django-cleanup/
- :alt: PyPI Package
- .. |Status| image:: https://travis-ci.org/un1t/django-cleanup.svg?branch=master
- :target: https://travis-ci.org/un1t/django-cleanup
- :alt: Build Status
- .. |License| image:: https://img.shields.io/badge/license-MIT-maroon
- :target: https://github.com/un1t/django-cleanup/blob/master/LICENSE
- :alt: MIT License
-
-Keywords: django
-Platform: UNKNOWN
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Environment :: Web Environment
-Classifier: Framework :: Django
-Classifier: Framework :: Django :: 2.2
-Classifier: Framework :: Django :: 3.0
-Classifier: Framework :: Django :: 3.1
-Classifier: Framework :: Django :: 3.2
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: MIT License
-Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: Implementation :: CPython
-Classifier: Programming Language :: Python :: Implementation :: PyPy
-Classifier: Topic :: Utilities
-Requires: python (>=3.5)
-Requires: django (>=2.2)
-Description-Content-Type: text/x-rst
diff --git a/django_cleanup.egg-info/SOURCES.txt b/django_cleanup.egg-info/SOURCES.txt
deleted file mode 100644
index c92e32f..0000000
--- a/django_cleanup.egg-info/SOURCES.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-CHANGELOG.md
-LICENSE
-MANIFEST.in
-README.rst
-setup.cfg
-setup.py
-django_cleanup/__init__.py
-django_cleanup/apps.py
-django_cleanup/cache.py
-django_cleanup/cleanup.py
-django_cleanup/handlers.py
-django_cleanup/signals.py
-django_cleanup.egg-info/PKG-INFO
-django_cleanup.egg-info/SOURCES.txt
-django_cleanup.egg-info/dependency_links.txt
-django_cleanup.egg-info/top_level.txt
\ No newline at end of file
diff --git a/django_cleanup/cleanup.py b/django_cleanup/cleanup.py
deleted file mode 100644
index ad7a163..0000000
--- a/django_cleanup/cleanup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-'''Public utilities'''
-from .cache import (
- get_mangled_ignore as _get_mangled_ignore, make_cleanup_cache as _make_cleanup_cache)
-
-
-__all__ = ['refresh', 'cleanup_ignore']
-
-
-def refresh(instance):
- '''Refresh the cache for an instance'''
- return _make_cleanup_cache(instance)
-
-
-def ignore(cls):
- '''Mark a model to ignore for cleanup'''
- setattr(cls, _get_mangled_ignore(cls), None)
- return cls
-cleanup_ignore = ignore
diff --git a/setup.py b/setup.py
index c902f20..cd318db 100644
--- a/setup.py
+++ b/setup.py
@@ -29,10 +29,11 @@ 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.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,20 +47,20 @@ setup(
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
'Framework :: Django',
- 'Framework :: Django :: 2.2',
- 'Framework :: Django :: 3.0',
- 'Framework :: Django :: 3.1',
'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',
'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/src/django_cleanup.egg-info/PKG-INFO b/src/django_cleanup.egg-info/PKG-INFO
new file mode 100644
index 0000000..98bf080
--- /dev/null
+++ b/src/django_cleanup.egg-info/PKG-INFO
@@ -0,0 +1,258 @@
+Metadata-Version: 2.1
+Name: django-cleanup
+Version: 7.0.0
+Summary: Deletes old files.
+Home-page: https://github.com/un1t/django-cleanup
+Download-URL: https://github.com/un1t/django-cleanup/tarball/master
+Author: Ilya Shalyapin
+Author-email: ishalyapin@gmail.com
+License: MIT License
+Keywords: django
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Framework :: Django
+Classifier: Framework :: Django :: 3.2
+Classifier: Framework :: Django :: 4.0
+Classifier: Framework :: Django :: 4.1
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Utilities
+Requires: python (>=3.6)
+Requires: django (>=3.2)
+Description-Content-Type: text/x-rst
+License-File: LICENSE
+
+Django Cleanup
+**************
+|Version| |Status| |License|
+
+Features
+========
+The django-cleanup app automatically deletes files for :code:`FileField`, :code:`ImageField` and
+subclasses. When a :code:`FileField`'s value is changed and the model is saved, the old file is
+deleted. When a model that has a :code:`FileField` is deleted, the file is also deleted. A file that
+is set as the :code:`FileField`'s default value will not be deleted.
+
+Compatibility
+-------------
+- Django 3.2, 4.0, 4.1 (`See Django Supported Versions <https://www.djangoproject.com/download/#supported-versions>`_)
+- Python 3.6+
+- Compatible with `sorl-thumbnail <https://github.com/jazzband/sorl-thumbnail>`_
+- Compatible with `easy-thumbnail <https://github.com/SmileyChris/easy-thumbnails>`_
+
+How does it work?
+=================
+In order to track changes of a :code:`FileField` and facilitate file deletions, django-cleanup
+connects :code:`post_init`, :code:`pre_save`, :code:`post_save` and :code:`post_delete` signals to
+signal handlers for each :code:`INSTALLED_APPS` model that has a :code:`FileField`. In order to tell
+whether or not a :code:`FileField`'s value has changed a local cache of original values is kept on
+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!**
+
+Installation
+============
+::
+
+ pip install django-cleanup
+
+
+Configuration
+=============
+Add ``django_cleanup`` to the bottom of ``INSTALLED_APPS`` in ``settings.py``
+
+.. code-block:: py
+
+ INSTALLED_APPS = (
+ ...,
+ 'django_cleanup.apps.CleanupConfig',
+ )
+
+That is all, no other configuration is necessary.
+
+Note: Order of ``INSTALLED_APPS`` is important. To ensure that exceptions inside other apps' signal
+handlers do not affect the integrity of file deletions within transactions, ``django_cleanup``
+should be placed last in ``INSTALLED_APPS``.
+
+Troubleshooting
+===============
+If you notice that ``django-cleanup`` is not removing files when expected, check that your models
+are being properly
+`loaded <https://docs.djangoproject.com/en/stable/ref/applications/#how-applications-are-loaded>`_:
+
+ You must define or import all models in your application's models.py or models/__init__.py.
+ Otherwise, the application registry may not be fully populated at this point, which could cause
+ the ORM to malfunction.
+
+If your models are not loaded, ``django-cleanup`` will not be able to discover their
+``FileField``'s.
+
+You can check if your ``Model`` is loaded by using
+
+.. code-block:: py
+
+ 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. 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
+========
+This section contains additional functionality that can be used to interact with django-cleanup for
+special cases.
+
+Signals
+-------
+To facilitate interactions with other django apps django-cleanup sends the following signals which
+can be imported from :code:`django_cleanup.signals`:
+
+- :code:`cleanup_pre_delete`: just before a file is deleted. Passes a :code:`file` keyword argument.
+- :code:`cleanup_post_delete`: just after a file is deleted. Passes a :code:`file` keyword argument.
+
+Signals example for sorl.thumbnail:
+
+.. code-block:: py
+
+ from django_cleanup.signals import cleanup_pre_delete
+ from sorl.thumbnail import delete
+
+ def sorl_delete(**kwargs):
+ delete(kwargs['file'])
+
+ cleanup_pre_delete.connect(sorl_delete)
+
+Refresh the cache
+-----------------
+There have been rare cases where the cache would need to be refreshed. To do so the
+:code:`django_cleanup.cleanup.refresh` method can be used:
+
+.. code-block:: py
+
+ from django_cleanup import cleanup
+
+ cleanup.refresh(model_instance)
+
+Ignore cleanup for a specific model
+-----------------------------------
+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
+
+ from django_cleanup import cleanup
+
+ @cleanup.ignore
+ 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 ':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
+
+ 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
+(see the `tox.ini <https://github.com/un1t/django-cleanup/blob/master/tox.ini>`_).
+
+Setup pyenv_ to have all versions of python activated within your local django-cleanup repository.
+Ensuring that the python 3.10 that was installed is first priority.
+
+Install tox_ on python 3.10 and run the :code:`tox` command from your local django-cleanup
+repository.
+
+How to write tests
+==================
+This app requires the use of django.test.TransactionTestCase_ when writing tests.
+
+For details on why this is required see `here
+<https://docs.djangoproject.com/en/stable/topics/db/transactions/#use-in-tests>`_:
+
+ Django's :code:`TestCase` class wraps each test in a transaction and rolls back that transaction
+ after each test, in order to provide test isolation. This means that no transaction is ever
+ actually committed, thus your :code:`on_commit()` callbacks will never be run. If you need to
+ test the results of an :code:`on_commit()` callback, use a :code:`TransactionTestCase` instead.
+
+License
+=======
+django-cleanup is free software under terms of the:
+
+MIT License
+
+Copyright (C) 2012 by Ilya Shalyapin, ishalyapin@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+.. _django.test.TransactionTestCase: https://docs.djangoproject.com/en/stable/topics/testing/tools/#django.test.TransactionTestCase
+.. _pyenv: https://github.com/pyenv/pyenv
+.. _tox: https://tox.readthedocs.io/en/latest/
+
+.. |Version| image:: https://img.shields.io/pypi/v/django-cleanup.svg
+ :target: https://pypi.python.org/pypi/django-cleanup/
+ :alt: PyPI Package
+.. |Status| image:: https://github.com/un1t/django-cleanup/actions/workflows/main.yml/badge.svg
+ :target: https://github.com/un1t/django-cleanup/actions/workflows/main.yml
+ :alt: Build Status
+.. |License| image:: https://img.shields.io/badge/license-MIT-maroon
+ :target: https://github.com/un1t/django-cleanup/blob/master/LICENSE
+ :alt: MIT License
diff --git a/src/django_cleanup.egg-info/SOURCES.txt b/src/django_cleanup.egg-info/SOURCES.txt
new file mode 100644
index 0000000..be7154b
--- /dev/null
+++ b/src/django_cleanup.egg-info/SOURCES.txt
@@ -0,0 +1,19 @@
+CHANGELOG.md
+LICENSE
+MANIFEST.in
+README.rst
+setup.cfg
+setup.py
+src/django_cleanup/__init__.py
+src/django_cleanup/apps.py
+src/django_cleanup/cache.py
+src/django_cleanup/cleanup.py
+src/django_cleanup/handlers.py
+src/django_cleanup/signals.py
+src/django_cleanup.egg-info/PKG-INFO
+src/django_cleanup.egg-info/SOURCES.txt
+src/django_cleanup.egg-info/dependency_links.txt
+src/django_cleanup.egg-info/top_level.txt
+test/test_all.py
+test/test_integration.py
+test/testing_helpers.py
\ No newline at end of file
diff --git a/django_cleanup.egg-info/dependency_links.txt b/src/django_cleanup.egg-info/dependency_links.txt
similarity index 100%
rename from django_cleanup.egg-info/dependency_links.txt
rename to src/django_cleanup.egg-info/dependency_links.txt
diff --git a/django_cleanup.egg-info/top_level.txt b/src/django_cleanup.egg-info/top_level.txt
similarity index 100%
rename from django_cleanup.egg-info/top_level.txt
rename to src/django_cleanup.egg-info/top_level.txt
diff --git a/django_cleanup/__init__.py b/src/django_cleanup/__init__.py
similarity index 73%
rename from django_cleanup/__init__.py
rename to src/django_cleanup/__init__.py
index ee849fa..89b31a9 100644
--- a/django_cleanup/__init__.py
+++ b/src/django_cleanup/__init__.py
@@ -3,5 +3,5 @@
subclasses. It will delete old files when a new file is being save and it
will delete files on model instance deletion.
'''
-__version__ = '5.2.0'
-default_app_config = 'django_cleanup.apps.CleanupConfig'
+
+__version__ = '7.0.0'
diff --git a/django_cleanup/apps.py b/src/django_cleanup/apps.py
similarity index 56%
rename from django_cleanup/apps.py
rename to src/django_cleanup/apps.py
index fd22910..002965f 100644
--- a/django_cleanup/apps.py
+++ b/src/django_cleanup/apps.py
@@ -9,7 +9,16 @@ from . import cache, handlers
class CleanupConfig(AppConfig):
name = 'django_cleanup'
verbose_name = 'Django Cleanup'
+ default = True
def ready(self):
- cache.prepare()
+ cache.prepare(False)
+ 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/src/django_cleanup/cache.py
similarity index 89%
rename from django_cleanup/cache.py
rename to src/django_cleanup/cache.py
index b34fcf9..a2e3423 100644
--- a/django_cleanup/cache.py
+++ b/src/django_cleanup/cache.py
@@ -24,13 +24,13 @@ DOTTED_PATH = '{klass.__module__}.{klass.__qualname__}'
# cache init ##
-def prepare():
+def prepare(select_mode):
'''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,15 @@ 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 +130,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/src/django_cleanup/cleanup.py b/src/django_cleanup/cleanup.py
new file mode 100644
index 0000000..eaa17b3
--- /dev/null
+++ b/src/django_cleanup/cleanup.py
@@ -0,0 +1,26 @@
+'''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)
+
+
+__all__ = ['refresh', 'cleanup_ignore', 'cleanup_select']
+
+
+def refresh(instance):
+ '''Refresh the cache for an instance'''
+ return _make_cleanup_cache(instance)
+
+
+def ignore(cls):
+ '''Mark a model to ignore for cleanup'''
+ 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
diff --git a/django_cleanup/handlers.py b/src/django_cleanup/handlers.py
similarity index 83%
rename from django_cleanup/handlers.py
rename to src/django_cleanup/handlers.py
index e40240c..aa6a8c5 100644
--- a/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,34 @@ 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',
+ 'model_name': model_name,
+ 'field_name': field_name,
+ 'file_name': file_.name,
+ 'default_file_name': default,
+ '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
+ error = None
try:
file_.delete(save=False)
- except Exception:
+ success = True
+ 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=None, file=file_)
+ cleanup_post_delete.send(sender=sender, error=error, success=success, **event)
on_commit(run_on_commit, using)
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/test/test_all.py b/test/test_all.py
new file mode 100644
index 0000000..1f1692a
--- /dev/null
+++ b/test/test_all.py
@@ -0,0 +1,452 @@
+import logging
+import os
+import pickle
+import re
+import tempfile
+
+from django.conf import settings
+from django.core.files import File
+from django.db import transaction
+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 (
+ BranchProduct, Product, ProductIgnore, ProductProxy, ProductUnmanaged, RootProduct)
+from .testing_helpers import get_random_pic_name, get_using
+
+
+LINE = re.compile(r'line \d{1,3}')
+
+TB = '''Traceback (most recent call last):
+ File "{handlers}", line xxx, in run_on_commit
+ file_.delete(save=False)
+ File "{files}", line xxx, in delete
+ self.storage.delete(self.name)
+ File "{storage}", line xxx, in delete
+ os.remove(name)
+{error}: [Errno 2] No such file or directory: '{{picture}}\''''
+
+
+
+def getTraceback():
+ fileabspath = os.path.abspath
+ error = 'FileNotFoundError'
+
+ return TB.format(
+ handlers=fileabspath(handlers.__file__),
+ files=fileabspath(files.__file__),
+ storage=fileabspath(storage.__file__),
+ error=error)
+
+
+def _raise(message):
+ def _func(x): # pragma: no cover
+ raise Exception(message)
+ return _func
+
+
+@pytest.mark.django_db(transaction=True)
+def test_refresh_from_db_without_refresh(picture):
+ product = Product.objects.create(image=picture['filename'])
+ assert os.path.exists(picture['path'])
+ product.refresh_from_db()
+ assert id(product.image.instance) == id(product)
+ product.image = get_random_pic_name()
+ with transaction.atomic(get_using(product)):
+ product.save()
+ assert not os.path.exists(picture['path'])
+
+
+@pytest.mark.django_db(transaction=True)
+def test_cache_gone(picture):
+ product = Product.objects.create(image=picture['filename'])
+ assert os.path.exists(picture['path'])
+ product.image = get_random_pic_name()
+ cache.remove_instance_cache(product)
+ with transaction.atomic(get_using(product)):
+ product.save()
+ assert not os.path.exists(picture['path'])
+
+
+@pytest.mark.django_db(transaction=True)
+def test_storage_gone(picture):
+ product = Product.objects.create(image=picture['filename'])
+ assert os.path.exists(picture['path'])
+ product.image = get_random_pic_name()
+ product = pickle.loads(pickle.dumps(product))
+ assert hasattr(product.image, 'storage')
+ with transaction.atomic(get_using(product)):
+ product.save()
+ assert not os.path.exists(picture['path'])
+
+
+@pytest.mark.django_db(transaction=True)
+def test_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.django_db(transaction=True)
+def test_replace_file_with_blank(picture):
+ product = Product.objects.create(image=picture['filename'])
+ assert os.path.exists(picture['path'])
+ product.image = ''
+ with transaction.atomic(get_using(product)):
+ product.save()
+ assert not os.path.exists(picture['path'])
+ assert not product.image
+ assert product.image.name == ''
+
+
+@pytest.mark.django_db(transaction=True)
+def test_replace_file_with_none(picture):
+ product = Product.objects.create(image=picture['filename'])
+ assert os.path.exists(picture['path'])
+ product.image = None
+ with transaction.atomic(get_using(product)):
+ product.save()
+ assert not os.path.exists(picture['path'])
+ assert not product.image
+ assert product.image.name is None
+
+
+@pytest.mark.django_db(transaction=True)
+def test_replace_file_proxy(picture):
+ product = ProductProxy.objects.create(image=picture['filename'])
+ assert os.path.exists(picture['path'])
+ product.image = get_random_pic_name()
+ with transaction.atomic(get_using(product)):
+ product.save()
+ assert not os.path.exists(picture['path'])
+
+
+@pytest.mark.django_db(transaction=True)
+def test_replace_file_unmanaged(picture):
+ product = ProductUnmanaged.objects.create(image=picture['filename'])
+ assert os.path.exists(picture['path'])
+ product.image = get_random_pic_name()
+ with transaction.atomic(get_using(product)):
+ product.save()
+ assert not os.path.exists(picture['path'])
+
+
+@pytest.mark.django_db(transaction=True)
+def test_replace_file_deferred(picture):
+ '''probably shouldn't save from a deferred model but someone might do it'''
+ product = Product.objects.create(image=picture['filename'])
+ assert os.path.exists(picture['path'])
+ product_deferred = Product.objects.defer('image_default').get(id=product.id)
+ product_deferred.image = get_random_pic_name()
+ with transaction.atomic(get_using(product)):
+ product_deferred.save()
+ assert not os.path.exists(picture['path'])
+
+
+@pytest.mark.django_db(transaction=True)
+def test_remove_model_instance(picture):
+ product = Product.objects.create(image=picture['filename'])
+ assert os.path.exists(picture['path'])
+ with transaction.atomic(get_using(product)):
+ product.delete()
+ assert not os.path.exists(picture['path'])
+
+
+@pytest.mark.django_db(transaction=True)
+def test_remove_model_instance_default(picture):
+ product = Product.objects.create()
+ assert product.image_default.path == picture['srcpath']
+ assert product.image_default_callable.path == picture['srcpath']
+ assert os.path.exists(picture['srcpath'])
+ with transaction.atomic(get_using(product)):
+ product.delete()
+ assert os.path.exists(picture['srcpath'])
+
+
+@pytest.mark.django_db(transaction=True)
+def test_replace_file_with_file_default(picture):
+ product = Product.objects.create()
+ assert os.path.exists(picture['srcpath'])
+ random_pic_name1 = get_random_pic_name()
+ random_pic_name2 = get_random_pic_name()
+ product.image_default = random_pic_name1
+ product.image_default_callable = random_pic_name2
+ with transaction.atomic(get_using(product)):
+ product.save()
+ assert os.path.exists(picture['srcpath'])
+
+
+@pytest.mark.django_db(transaction=True)
+def test_remove_model_instance_ignore(picture):
+ product = ProductIgnore.objects.create(image=picture['filename'])
+ assert os.path.exists(picture['path'])
+ with transaction.atomic(get_using(product)):
+ product.delete()
+ assert os.path.exists(picture['path'])
+
+
+@pytest.mark.django_db(transaction=True)
+def test_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
+
+
+@pytest.mark.django_db(transaction=True)
+def test_remove_model_instance_proxy(picture):
+ product = ProductProxy.objects.create(image=picture['filename'])
+ assert os.path.exists(picture['path'])
+ with transaction.atomic(get_using(product)):
+ product.delete()
+ assert not os.path.exists(picture['path'])
+
+
+@pytest.mark.django_db(transaction=True)
+def test_remove_model_instance_unmanaged(picture):
+ product = ProductUnmanaged.objects.create(image=picture['filename'])
+ assert os.path.exists(picture['path'])
+ with transaction.atomic(get_using(product)):
+ product.delete()
+ assert not os.path.exists(picture['path'])
+
+
+@pytest.mark.django_db(transaction=True)
+def test_remove_model_instance_deferred(picture):
+ product = Product.objects.create(image=picture['filename'])
+ assert os.path.exists(picture['path'])
+ product_deferred = Product.objects.defer('image_default').get(id=product.id)
+ with transaction.atomic(get_using(product)):
+ product_deferred.delete()
+ assert not os.path.exists(picture['path'])
+
+
+@pytest.mark.django_db(transaction=True)
+def test_remove_blank_file(monkeypatch):
+ product = Product.objects.create(image='')
+ monkeypatch.setattr(
+ product.image.storage, 'exists', _raise('should not call exists'))
+ monkeypatch.setattr(
+ product.image.storage, 'delete', _raise('should not call delete'))
+ with transaction.atomic(get_using(product)):
+ product.delete()
+
+
+@pytest.mark.django_db(transaction=True)
+def test_remove_not_exists():
+ product = Product.objects.create(image='no-such-file')
+ with transaction.atomic(get_using(product)):
+ product.delete()
+
+
+@pytest.mark.django_db(transaction=True)
+def test_remove_none(monkeypatch):
+ product = Product.objects.create(image=None)
+ monkeypatch.setattr(
+ product.image.storage, 'exists', _raise('should not call exists'))
+ monkeypatch.setattr(
+ product.image.storage, 'delete', _raise('should not call delete'))
+ with transaction.atomic(get_using(product)):
+ product.delete()
+
+
+@pytest.mark.django_db(transaction=True)
+def test_exception_on_save(settings, picture, caplog):
+ 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'])
+ product.image.delete(save=False)
+ product.image = None
+ assert not os.path.exists(picture['path'])
+ with transaction.atomic(get_using(product)):
+ product.save()
+ assert not os.path.exists(picture['path'])
+
+ for record in caplog.records:
+ assert LINE.sub('line xxx', record.exc_text) == getTraceback().format(picture=picture['path'])
+ assert caplog.record_tuples == [
+ (
+ 'django_cleanup.handlers',
+ logging.ERROR,
+ 'There was an exception deleting the file `{}` on field `test.product.image`'.format(
+ picture['filename'])
+ )
+ ]
+
+
+@pytest.mark.django_db(transaction=True)
+def test_cascade_delete(picture):
+ root = RootProduct.objects.create()
+ branch = BranchProduct.objects.create(root=root, image=picture['filename'])
+ assert os.path.exists(picture['path'])
+ root = RootProduct.objects.get()
+ with transaction.atomic(get_using(root)):
+ root.delete()
+ assert not os.path.exists(picture['path'])
+
+
+@pytest.mark.django_db(transaction=True)
+def test_file_exists_on_create_and_update():
+ # If a filepath is specified which already exists,
+ # the FileField generates a random suffix to choose a different location.
+ # We need to make sure, that we fetch this change and would delete the correct one
+ # on further edits or the final deletion.
+ # In this test case it is simulated by using a temporary file located
+ # directly within the same directory as the image would be uploaded to.
+
+ upload_to = Product._meta.get_field("image").upload_to
+ dst_directory = os.path.join(settings.MEDIA_ROOT, upload_to)
+ if not os.path.isdir(dst_directory):
+ os.makedirs(dst_directory)
+
+ # create the new product with a file to simulate an "upload"
+ # a file aleady exists so the new file is renamed then saved
+ with tempfile.NamedTemporaryFile(prefix="f1__", dir=dst_directory) as f1:
+ with transaction.atomic():
+ product = Product.objects.create(image=File(f1, name=os.path.join(upload_to, os.path.basename(f1.name))))
+
+ assert f1.name != product.image.path
+ assert os.path.exists(f1.name)
+ assert os.path.exists(product.image.path)
+
+ path_prior_to_edit = product.image.path
+
+ # edit the product to change the product file to a different file
+ # check that it deletes the renamed file, not the original existing file
+ with tempfile.NamedTemporaryFile(prefix="f2__", dir=dst_directory) as f2:
+ with transaction.atomic(get_using(product)):
+ product.image = File(f2, name=os.path.join(upload_to, os.path.basename(f2.name)))
+ assert f2.name == product.image.path
+ product.save()
+
+ assert f1.name != product.image.path
+ assert os.path.exists(f1.name)
+ assert f2.name != product.image.path
+ assert os.path.isfile(f2.name)
+ assert os.path.isfile(product.image.path)
+ assert not os.path.isfile(path_prior_to_edit)
+
+ with transaction.atomic(get_using(product)):
+ product.delete()
+
+ assert os.path.isfile(f1.name)
+ assert os.path.isfile(f2.name)
+ 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)
+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/test/test_integration.py b/test/test_integration.py
new file mode 100644
index 0000000..f693a4f
--- /dev/null
+++ b/test/test_integration.py
@@ -0,0 +1,90 @@
+import os
+
+from django.conf import settings
+from django.db import transaction
+
+import pytest
+
+from django_cleanup.signals import cleanup_pre_delete
+
+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("test.models.integration")
+ ProductIntegration = models.ProductIntegration
+ sorl_delete = models.sorl_delete
+ cleanup_pre_delete.connect(sorl_delete)
+ from sorl.thumbnail import get_thumbnail
+ product = ProductIntegration.objects.create(sorl_image=picture['filename'])
+ assert os.path.exists(picture['path'])
+ im = get_thumbnail(
+ product.sorl_image, '100x100', crop='center', quality=50)
+ thumbnail_path = os.path.join(settings.MEDIA_ROOT, im.name)
+ assert os.path.exists(thumbnail_path)
+ product.sorl_image = 'new.png'
+ with transaction.atomic(get_using(product)):
+ product.save()
+ assert not os.path.exists(picture['path'])
+ assert not os.path.exists(thumbnail_path)
+ cleanup_pre_delete.disconnect(sorl_delete)
+
+
+@pytest.mark.django_db(transaction=True)
+def test_sorlthumbnail_delete(picture):
+ # https://github.com/mariocesar/sorl-thumbnail
+ models = pytest.importorskip("test.models.integration")
+ ProductIntegration = models.ProductIntegration
+ sorl_delete = models.sorl_delete
+ cleanup_pre_delete.connect(sorl_delete)
+ from sorl.thumbnail import get_thumbnail
+ product = ProductIntegration.objects.create(sorl_image=picture['filename'])
+ assert os.path.exists(picture['path'])
+ im = get_thumbnail(
+ product.sorl_image, '100x100', crop='center', quality=50)
+ thumbnail_path = os.path.join(settings.MEDIA_ROOT, im.name)
+ assert os.path.exists(thumbnail_path)
+ with transaction.atomic(get_using(product)):
+ product.delete()
+ assert not os.path.exists(picture['path'])
+ assert not os.path.exists(thumbnail_path)
+ cleanup_pre_delete.disconnect(sorl_delete)
+
+
+@pytest.mark.django_db(transaction=True)
+def test_easythumbnails_replace(picture):
+ # https://github.com/SmileyChris/easy-thumbnails
+ models = pytest.importorskip("test.models.integration")
+ ProductIntegration = models.ProductIntegration
+ from easy_thumbnails.files import get_thumbnailer
+ product = ProductIntegration.objects.create(easy_image=picture['filename'])
+ assert os.path.exists(picture['path'])
+ im = get_thumbnailer(product.easy_image).get_thumbnail(
+ {'size': (100, 100)})
+ thumbnail_path = os.path.join(settings.MEDIA_ROOT, im.name)
+ assert os.path.exists(thumbnail_path)
+ product.easy_image = 'new.png'
+ with transaction.atomic(get_using(product)):
+ product.save()
+ assert not os.path.exists(picture['path'])
+ assert not os.path.exists(thumbnail_path)
+
+
+@pytest.mark.django_db(transaction=True)
+def test_easythumbnails_delete(picture):
+ # https://github.com/SmileyChris/easy-thumbnails
+ models = pytest.importorskip("test.models.integration")
+ ProductIntegration = models.ProductIntegration
+ from easy_thumbnails.files import get_thumbnailer
+ product = ProductIntegration.objects.create(easy_image=picture['filename'])
+ assert os.path.exists(picture['path'])
+ im = get_thumbnailer(product.easy_image).get_thumbnail(
+ {'size': (100, 100)})
+ thumbnail_path = os.path.join(settings.MEDIA_ROOT, im.name)
+ assert os.path.exists(thumbnail_path)
+ with transaction.atomic(get_using(product)):
+ product.delete()
+ assert not os.path.exists(picture['path'])
+ assert not os.path.exists(thumbnail_path)
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)))