diff --git a/.editorconfig b/.editorconfig index 058a363..67db007 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -# http://editorconfig.org +# https://editorconfig.org root = true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f367713..640bf1b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,14 +11,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.x - name: Install dependencies run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6a01614..20b5bc2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,6 +2,9 @@ name: Test on: [push, pull_request] +env: + FORCE_COLOR: 1 + jobs: build: runs-on: ubuntu-latest @@ -9,7 +12,7 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12-dev'] services: postgres: @@ -27,10 +30,10 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -40,7 +43,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: @@ -64,6 +67,6 @@ jobs: DB_PORT: 5432 - name: Upload coverage - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 with: name: Python ${{ matrix.python-version }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b00e484..d00ba3d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/PyCQA/isort - rev: 5.11.1 + rev: 5.12.0 hooks: - id: isort args: ['--profile', 'black', '--check-only', '--diff'] @@ -14,7 +14,7 @@ repos: files: ^(model_utils|tests)/ - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.6.0 hooks: - id: pyupgrade - args: [--py37-plus] + args: [--py38-plus] diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..d4eb136 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,18 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +# Project page: https://readthedocs.org/projects/django-model-utils/ + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3" + +python: + install: + - method: pip + path: . + +sphinx: + configuration: docs/conf.py diff --git a/CHANGES.rst b/CHANGES.rst index 3ac443a..4cb100e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,8 +1,17 @@ Changelog ========= -4.3.2 ------ +To be released +-------- + +- Remove ``SaveSignalHandlingModel``. This model used a modified copy of the internal Django method `Model.save_base()` + and had not been updated for upstream bug fixes changes since its addition. (GH-#582) +- Confirm support for `Django 4.2` +- Add support for `Python 3.11` (GH-#545) +- Add support for `Python 3.12` (GH-#545) +- Drop support for `Python 3.7` (GH-#545) +- Swedish translation (GH-#561) +- Use proper column name instead of attname (GH-#573) - Fix `ValueError` when calling `prefetch_related` for tracked `ForeignKey` fields (Fixes GH-433) 4.3.1 (2022-11-15) @@ -253,7 +262,7 @@ Changelog for the report. Thanks Matthew Schinckel for the fix. Merge of GH-130, fixes GH-83. -.. _IPython: http://ipython.org/ +.. _IPython: https://ipython.org/ 2.0.3 (2014.03.19) diff --git a/docs/Makefile b/docs/Makefile index 452f59f..c6c45a3 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -9,7 +9,7 @@ BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://sphinx-doc.org/) endif # Internal variables. diff --git a/docs/conf.py b/docs/conf.py index 0d47d51..fbab4aa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,7 +11,7 @@ # serve to show the default. import os -from pkg_resources import get_distribution +import importlib.metadata # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -49,7 +49,8 @@ parent_dir = os.path.dirname(os.path.dirname(__file__)) # |version| and |release|, also used in various other places throughout the # built documents. # -release = get_distribution('django-model-utils').version +release = importlib.metadata.version('django-model-utils') + # for example take major/minor version = '.'.join(release.split('.')[:2]) diff --git a/docs/make.bat b/docs/make.bat index fb1a0ef..b593358 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -56,7 +56,7 @@ if errorlevel 9009 ( echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ + echo.https://sphinx-doc.org/ exit /b 1 ) diff --git a/docs/managers.rst b/docs/managers.rst index 0d07a96..54aff0a 100644 --- a/docs/managers.rst +++ b/docs/managers.rst @@ -84,7 +84,7 @@ If you don't explicitly call ``select_subclasses()`` or ``get_subclass()``, an ``InheritanceManager`` behaves identically to a normal ``Manager``; so it's safe to use as your default manager for the model. -.. _contributed by Jeff Elmore: http://jeffelmore.org/2010/11/11/automatic-downcasting-of-inherited-models-in-django/ +.. _contributed by Jeff Elmore: https://jeffelmore.org/2010/11/11/automatic-downcasting-of-inherited-models-in-django/ JoinManager ----------- diff --git a/docs/models.rst b/docs/models.rst index 8ecf6bc..2c5996f 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -112,21 +112,3 @@ Also you can override the default uuid version. Versions 1,3,4 and 5 are now sup .. _`UUIDField`: https://github.com/jazzband/django-model-utils/blob/master/docs/fields.rst#uuidfield - - -SaveSignalHandlingModel ------------------------ - -An abstract base class model to pass a parameter ``signals_to_disable`` -to ``save`` method in order to disable signals - -.. code-block:: python - - from model_utils.models import SaveSignalHandlingModel - - class SaveSignalTestModel(SaveSignalHandlingModel): - name = models.CharField(max_length=20) - - obj = SaveSignalTestModel(name='Test') - # Note: If you use `Model.objects.create`, the signals can't be disabled - obj.save(signals_to_disable=['pre_save'] # disable `pre_save` signal diff --git a/docs/setup.rst b/docs/setup.rst index 69c9840..ad01f8a 100644 --- a/docs/setup.rst +++ b/docs/setup.rst @@ -20,4 +20,4 @@ Dependencies ``django-model-utils`` supports `Django`_ 3.2+ (latest bugfix release in each series only) on Python 3.7+. -.. _Django: http://www.djangoproject.com/ +.. _Django: https://www.djangoproject.com/ diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 691cf5c..8b02a0e 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,10 +1,10 @@ -from pkg_resources import DistributionNotFound, get_distribution +import importlib.metadata from .choices import Choices # noqa:F401 from .tracker import FieldTracker, ModelTracker # noqa:F401 try: - __version__ = get_distribution("django-model-utils").version -except DistributionNotFound: # pragma: no cover + __version__ = importlib.metadata.version('django-model-utils') +except importlib.metadata.PackageNotFoundError: # pragma: no cover # package is not installed __version__ = None diff --git a/model_utils/locale/sv/LC_MESSAGES/django.mo b/model_utils/locale/sv/LC_MESSAGES/django.mo new file mode 100644 index 0000000..6745da7 Binary files /dev/null and b/model_utils/locale/sv/LC_MESSAGES/django.mo differ diff --git a/model_utils/locale/sv/LC_MESSAGES/django.po b/model_utils/locale/sv/LC_MESSAGES/django.po new file mode 100644 index 0000000..6db0c00 --- /dev/null +++ b/model_utils/locale/sv/LC_MESSAGES/django.po @@ -0,0 +1,53 @@ +# This file is distributed under the same license as the django-model-utils package. +# +# Translators: +# Tomas Walch , 2022. +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-11-23 14:46+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +#: model_utils/models.py:25 +msgid "created" +msgstr "skapad" + +#: model_utils/models.py:26 +msgid "modified" +msgstr "ändrad" + +#: model_utils/models.py:50 +msgid "start" +msgstr "start" + +#: model_utils/models.py:51 +msgid "end" +msgstr "slut" + +#: model_utils/models.py:66 +msgid "status" +msgstr "status" + +#: model_utils/models.py:67 +msgid "status changed" +msgstr "status ändrad" + +#: tests/models.py:106 tests/models.py:115 tests/models.py:124 +msgid "active" +msgstr "aktiv" + +#: tests/models.py:107 tests/models.py:116 tests/models.py:125 +msgid "deleted" +msgstr "borttagen" + +#: tests/models.py:108 tests/models.py:117 tests/models.py:126 +msgid "on hold" +msgstr "väntande" + diff --git a/model_utils/managers.py b/model_utils/managers.py index 75d3d98..135629c 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -194,7 +194,7 @@ class InheritanceQuerySet(InheritanceQuerySetMixin, QuerySet): where_queries.append('(' + ' AND '.join([ '"{}"."{}" IS NOT NULL'.format( model._meta.db_table, - field.attname, # Should this be something else? + field.column, ) for field in model._meta.parents.values() ]) + ')') diff --git a/model_utils/models.py b/model_utils/models.py index 268db8c..c816b46 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -1,7 +1,6 @@ from django.core.exceptions import ImproperlyConfigured -from django.db import models, router, transaction +from django.db import models from django.db.models.functions import Now -from django.db.models.signals import post_save, pre_save from django.utils.translation import gettext_lazy as _ from model_utils.fields import ( @@ -172,60 +171,3 @@ class UUIDModel(models.Model): class Meta: abstract = True - - -class SaveSignalHandlingModel(models.Model): - """ - An abstract base class model to pass a parameter ``signals_to_disable`` - to ``save`` method in order to disable signals - """ - class Meta: - abstract = True - - def save(self, signals_to_disable=None, *args, **kwargs): - """ - Add an extra parameters to hold which signals to disable - If empty, nothing will change - """ - - self.signals_to_disable = signals_to_disable or [] - - super().save(*args, **kwargs) - - def save_base(self, raw=False, force_insert=False, - force_update=False, using=None, update_fields=None): - """ - Copied from base class for a minor change. - This is an ugly overwriting but since Django's ``save_base`` method - does not differ between versions 1.8 and 1.10, - that way of implementing wouldn't harm the flow - """ - using = using or router.db_for_write(self.__class__, instance=self) - assert not (force_insert and (force_update or update_fields)) - assert update_fields is None or len(update_fields) > 0 - cls = origin = self.__class__ - - if cls._meta.proxy: - cls = cls._meta.concrete_model - meta = cls._meta - if not meta.auto_created and 'pre_save' not in self.signals_to_disable: - pre_save.send( - sender=origin, instance=self, raw=raw, using=using, - update_fields=update_fields, - ) - with transaction.atomic(using=using, savepoint=False): - if not raw: - self._save_parents(cls, using, update_fields) - updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields) - - self._state.db = using - self._state.adding = False - - if not meta.auto_created and 'post_save' not in self.signals_to_disable: - post_save.send( - sender=origin, instance=self, created=(not updated), - update_fields=update_fields, raw=raw, using=using, - ) - - # Empty the signals in case it might be used somewhere else in future - self.signals_to_disable = [] diff --git a/model_utils/py.typed b/model_utils/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/requirements-test.txt b/requirements-test.txt index 62ed4cc..138b0de 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ pytest==6.2.5 pytest-django==3.10.0 -psycopg2-binary==2.8.6 +psycopg2-binary==2.9.5 pytest-cov==2.10.1 diff --git a/setup.py b/setup.py index 2c6f855..85834df 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ setup( maintainer='JazzBand', url='https://github.com/jazzband/django-model-utils', packages=find_packages(exclude=['tests*']), - python_requires=">=3.7", + python_requires=">=3.8", install_requires=['Django>=3.2'], classifiers=[ 'Development Status :: 5 - Production/Stable', @@ -39,19 +39,23 @@ setup( 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - '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 :: 3.12', 'Framework :: Django', 'Framework :: Django :: 3.2', 'Framework :: Django :: 4.0', 'Framework :: Django :: 4.1', + 'Framework :: Django :: 4.2', ], zip_safe=False, package_data={ 'model_utils': [ - 'locale/*/LC_MESSAGES/django.po', 'locale/*/LC_MESSAGES/django.mo' + 'locale/*/LC_MESSAGES/django.po', + 'locale/*/LC_MESSAGES/django.mo', + 'py.typed', ], }, ) diff --git a/tests/models.py b/tests/models.py index d785e88..b5efe55 100644 --- a/tests/models.py +++ b/tests/models.py @@ -7,7 +7,6 @@ from model_utils import Choices from model_utils.fields import MonitorField, SplitField, StatusField, UUIDField from model_utils.managers import InheritanceManager, JoinManagerMixin, QueryManager from model_utils.models import ( - SaveSignalHandlingModel, SoftDeletableModel, StatusModel, TimeFramedModel, @@ -67,6 +66,12 @@ class InheritanceManagerTestChild3(InheritanceManagerTestParent): parent_link=True, on_delete=models.CASCADE) +class InheritanceManagerTestChild3_1(InheritanceManagerTestParent): + parent_ptr = models.OneToOneField( + InheritanceManagerTestParent, db_column="custom_parent_ptr", + parent_link=True, on_delete=models.CASCADE) + + class InheritanceManagerTestChild4(InheritanceManagerTestParent): other_onetoone = models.OneToOneField( InheritanceManagerTestParent, related_name='non_inheritance_relation', @@ -440,10 +445,6 @@ class CustomNotPrimaryUUIDModel(models.Model): uuid = UUIDField(primary_key=False) -class SaveSignalHandlingTestModel(SaveSignalHandlingModel): - name = models.CharField(max_length=20) - - class TimeStampWithStatusModel(TimeStampedModel, StatusModel): STATUS = Choices( ("active", _("active")), diff --git a/tests/test_managers/test_inheritance_manager.py b/tests/test_managers/test_inheritance_manager.py index 102c6dd..60987bf 100644 --- a/tests/test_managers/test_inheritance_manager.py +++ b/tests/test_managers/test_inheritance_manager.py @@ -5,6 +5,7 @@ from tests.models import ( InheritanceManagerTestChild1, InheritanceManagerTestChild2, InheritanceManagerTestChild3, + InheritanceManagerTestChild3_1, InheritanceManagerTestChild4, InheritanceManagerTestGrandChild1, InheritanceManagerTestGrandChild1_2, @@ -141,6 +142,7 @@ class InheritanceManagerTests(TestCase): 'inheritancemanagertestchild1', 'inheritancemanagertestchild2', 'manual_onetoone', # this was set via parent_link & related_name + 'inheritancemanagertestchild3_1', 'child4_onetoone', ] self.assertEqual(set(results.subclasses), @@ -256,7 +258,9 @@ class InheritanceManagerUsingModelsTests(TestCase): objs = InheritanceManagerTestParent.objects.select_subclasses().order_by('pk') objsmodels = InheritanceManagerTestParent.objects.select_subclasses( InheritanceManagerTestChild1, InheritanceManagerTestChild2, - InheritanceManagerTestChild3, InheritanceManagerTestChild4, + InheritanceManagerTestChild3, + InheritanceManagerTestChild3_1, + InheritanceManagerTestChild4, InheritanceManagerTestGrandChild1, InheritanceManagerTestGrandChild1_2).order_by('pk') self.assertEqual(set(objs.subclasses), set(objsmodels.subclasses)) @@ -278,6 +282,7 @@ class InheritanceManagerUsingModelsTests(TestCase): models = (InheritanceManagerTestChild1, InheritanceManagerTestChild2, InheritanceManagerTestChild3, + InheritanceManagerTestChild3_1, InheritanceManagerTestChild4, InheritanceManagerTestGrandChild1, InheritanceManagerTestGrandChild1_2) @@ -426,6 +431,12 @@ class InheritanceManagerUsingModelsTests(TestCase): self.assertEqual([child3], list(results)) + def test_limit_to_specific_subclass_with_custom_db_column(self): + item = InheritanceManagerTestChild3_1.objects.create() + results = InheritanceManagerTestParent.objects.instance_of(InheritanceManagerTestChild3_1) + + self.assertEqual([item], list(results)) + def test_limit_to_specific_grandchild_class(self): grandchild1 = InheritanceManagerTestGrandChild1.objects.get() results = InheritanceManagerTestParent.objects.instance_of(InheritanceManagerTestGrandChild1) diff --git a/tests/test_models/test_savesignalhandling_model.py b/tests/test_models/test_savesignalhandling_model.py deleted file mode 100644 index 946da36..0000000 --- a/tests/test_models/test_savesignalhandling_model.py +++ /dev/null @@ -1,42 +0,0 @@ -from django.db.models.signals import post_save, pre_save -from django.test import TestCase - -from tests.models import SaveSignalHandlingTestModel -from tests.signals import post_save_test, pre_save_test - - -class SaveSignalHandlingModelTests(TestCase): - - def test_pre_save(self): - pre_save.connect(pre_save_test, sender=SaveSignalHandlingTestModel) - - obj = SaveSignalHandlingTestModel.objects.create(name='Test') - delattr(obj, 'pre_save_runned') - obj.name = 'Test A' - obj.save() - self.assertEqual(obj.name, 'Test A') - self.assertTrue(hasattr(obj, 'pre_save_runned')) - - obj = SaveSignalHandlingTestModel.objects.create(name='Test') - delattr(obj, 'pre_save_runned') - obj.name = 'Test B' - obj.save(signals_to_disable=['pre_save']) - self.assertEqual(obj.name, 'Test B') - self.assertFalse(hasattr(obj, 'pre_save_runned')) - - def test_post_save(self): - post_save.connect(post_save_test, sender=SaveSignalHandlingTestModel) - - obj = SaveSignalHandlingTestModel.objects.create(name='Test') - delattr(obj, 'post_save_runned') - obj.name = 'Test A' - obj.save() - self.assertEqual(obj.name, 'Test A') - self.assertTrue(hasattr(obj, 'post_save_runned')) - - obj = SaveSignalHandlingTestModel.objects.create(name='Test') - delattr(obj, 'post_save_runned') - obj.name = 'Test B' - obj.save(signals_to_disable=['post_save']) - self.assertEqual(obj.name, 'Test B') - self.assertFalse(hasattr(obj, 'post_save_runned')) diff --git a/tox.ini b/tox.ini index 1774e91..eb0c961 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = py{37,38,39,310}-dj32 - py{38,39,310}-dj{40,41,main} + py{38,39,310,311,312}-dj{40,41,42,main} flake8 isort @@ -11,6 +11,8 @@ python = 3.8: py38, flake8, isort 3.9: py39 3.10: py310 + 3.11: py311 + 3.12: py312 [testenv] deps = @@ -19,6 +21,7 @@ deps = dj32: Django==3.2.* dj40: Django==4.0.* dj41: Django==4.1.* + dj42: Django==4.2.* djmain: https://github.com/django/django/archive/main.tar.gz ignore_outcome = djmain: True @@ -26,11 +29,12 @@ ignore_errors = djmain: True passenv = CI + FORCE_COLOR GITHUB_* DB_* usedevelop = True commands = - pytest {posargs} + python -m pytest {posargs} [testenv:flake8] basepython =