From f8a7c50c0ac4e4db9fa0e13f2834fcb364e361bb Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 25 Jul 2014 09:51:50 -0600 Subject: [PATCH 001/271] Silence warning about MIDDLEWARE_CLASSES when running tests under 1.7. --- runtests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/runtests.py b/runtests.py index bb86dc7..a1f4d93 100755 --- a/runtests.py +++ b/runtests.py @@ -16,6 +16,7 @@ DEFAULT_SETTINGS = dict( "ENGINE": "django.db.backends.sqlite3" } }, + SILENCED_SYSTEM_CHECKS=["1_7.W001"], ) From 24fa8945bb4b5798c215241c173c372f5d2ea68c Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 25 Jul 2014 09:52:55 -0600 Subject: [PATCH 002/271] Bump version to 2.1.0. --- CHANGES.rst | 4 ++-- model_utils/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e39ccd5..5c605b8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,8 +1,8 @@ CHANGES ======= -master (unreleased) -------------------- +2.1.0 (2014.07.25) +------------------ * Add support for Django's built-in migrations to ``MonitorField`` and ``StatusField``. diff --git a/model_utils/__init__.py b/model_utils/__init__.py index d3ccdf2..9d81bc8 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.0.3.post1' +__version__ = '2.1.0' From a0ba93f8c2185145e9c3ee3a110a2ddabcac6062 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 25 Jul 2014 10:00:46 -0600 Subject: [PATCH 003/271] Bump version to 2.2a1. --- CHANGES.rst | 3 +++ model_utils/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5c605b8..de23aef 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,9 @@ CHANGES ======= +master (unreleased) +------------------- + 2.1.0 (2014.07.25) ------------------ diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 9d81bc8..2ee5bae 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.1.0' +__version__ = '2.2a1' From 6def1b416f22e9f665003f5e7433134195db2fd6 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 28 Jul 2014 09:55:18 -0600 Subject: [PATCH 004/271] ASCII-fold changelog, again. Fixes GH-141. --- CHANGES.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index de23aef..721dccb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,13 @@ CHANGES master (unreleased) ------------------- +* ASCII-fold all non-ASCII characters in changelog; again. Argh. Apologies to + those whose names are mangled by this change. It seems that distutils makes + it impossible to handle non-ASCII content reliably under Python 3 in a + setup.py long_description, when the system encoding may be ASCII. Thanks + Brian May for the report. Fixes GH-141. + + 2.1.0 (2014.07.25) ------------------ @@ -14,7 +21,7 @@ master (unreleased) ``dir``, allowing `IPython`_ tab completion to be useful. Merge of GH-104, fixes GH-55. -* Add pickle support for models using ``FieldTracker``. Thanks Ondrej Slinták +* Add pickle support for models using ``FieldTracker``. Thanks Ondrej Slintak for the report. Thanks Matthew Schinckel for the fix. Merge of GH-130, fixes GH-83. From 4ed7eb05e59d34838081e2ca19aae5c3a9fcc127 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 28 Jul 2014 09:59:23 -0600 Subject: [PATCH 005/271] Bump version to 2.1.1a. --- CHANGES.rst | 3 +++ model_utils/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5c605b8..cb0466c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,9 @@ CHANGES ======= +2.1.x (unreleased) +------------------ + 2.1.0 (2014.07.25) ------------------ diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 9d81bc8..23c8b70 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.1.0' +__version__ = '2.1.1a' From abd47ed4069f9360696205991f3f4fbbab4341ba Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 28 Jul 2014 09:55:18 -0600 Subject: [PATCH 006/271] ASCII-fold changelog, again. Fixes GH-141. --- CHANGES.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index cb0466c..561efde 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,13 @@ CHANGES 2.1.x (unreleased) ------------------ +* ASCII-fold all non-ASCII characters in changelog; again. Argh. Apologies to + those whose names are mangled by this change. It seems that distutils makes + it impossible to handle non-ASCII content reliably under Python 3 in a + setup.py long_description, when the system encoding may be ASCII. Thanks + Brian May for the report. Fixes GH-141. + + 2.1.0 (2014.07.25) ------------------ @@ -14,7 +21,7 @@ CHANGES ``dir``, allowing `IPython`_ tab completion to be useful. Merge of GH-104, fixes GH-55. -* Add pickle support for models using ``FieldTracker``. Thanks Ondrej Slinták +* Add pickle support for models using ``FieldTracker``. Thanks Ondrej Slintak for the report. Thanks Matthew Schinckel for the fix. Merge of GH-130, fixes GH-83. From bd3787a4a8a6d4b4dd1593b185f475b5d2b52126 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 28 Jul 2014 10:00:40 -0600 Subject: [PATCH 007/271] Bump version to 2.1.1. --- CHANGES.rst | 2 +- model_utils/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 561efde..a8ad9f5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,7 @@ CHANGES ======= -2.1.x (unreleased) +2.1.1 (2014.07.28) ------------------ * ASCII-fold all non-ASCII characters in changelog; again. Argh. Apologies to diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 23c8b70..38ac8b1 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.1.1a' +__version__ = '2.1.1' From 19c9c380df1335942277b2939ea7e7ae7bd743ed Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 28 Jul 2014 10:02:39 -0600 Subject: [PATCH 008/271] Bump version to 2.1.2a1. --- CHANGES.rst | 3 +++ model_utils/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index a8ad9f5..c5c7746 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,9 @@ CHANGES ======= +2.1.2a1 (unreleased) +-------------------- + 2.1.1 (2014.07.28) ------------------ diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 38ac8b1..95362ce 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.1.1' +__version__ = '2.1.2a1' From a127e32217b1abae3ef0ad543cf889d750be159e Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 31 Jul 2014 18:08:24 -0600 Subject: [PATCH 009/271] Revert "Use a signal handler instead of patching save." This reverts commit 3496fe42916696db36d799facfbf56f9caaa5035. --- model_utils/tests/tests.py | 3 --- model_utils/tracker.py | 38 +++++++++++++++++--------------------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index 9de6892..38150a8 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -1497,9 +1497,6 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): item.number = 2 self.assertTrue(item.tracker.has_changed('number')) - - def test_can_pickle_objects(self): - pickle.dumps(self.instance) class FieldTrackedModelCustomTests(FieldTrackerTestCase, diff --git a/model_utils/tracker.py b/model_utils/tracker.py index 021b12f..6cb4355 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -2,11 +2,10 @@ from __future__ import unicode_literals from copy import deepcopy -from django.core.exceptions import FieldError from django.db import models +from django.core.exceptions import FieldError from django.db.models.query_utils import DeferredAttribute -from django.db.models.signals import post_save -from django.dispatch import receiver + class FieldInstanceTracker(object): def __init__(self, instance, fields, field_map): @@ -120,16 +119,20 @@ class FieldTracker(object): models.signals.post_init.connect(self.initialize_tracker) self.model_class = sender setattr(sender, self.name, self) - - # Rather than patch the save method on the instance, - # we can observe the post_save signal on the class. - @receiver(post_save, sender=None, weak=False) - def handler(sender, instance, **kwargs): - if not isinstance(instance, self.model_class): - return - + + def initialize_tracker(self, sender, instance, **kwargs): + if not isinstance(instance, self.model_class): + return # Only init instances of given model (including children) + tracker = self.tracker_class(instance, self.fields, self.field_map) + setattr(instance, self.attname, tracker) + tracker.set_saved_fields() + self.patch_save(instance) + + def patch_save(self, instance): + original_save = instance.save + def save(**kwargs): + ret = original_save(**kwargs) update_fields = kwargs.get('update_fields') - if not update_fields and update_fields is not None: # () or [] fields = update_fields elif update_fields is None: @@ -139,19 +142,12 @@ class FieldTracker(object): field for field in update_fields if field in self.fields ) - getattr(instance, self.attname).set_saved_fields( fields=fields ) - + return ret + instance.save = save - def initialize_tracker(self, sender, instance, **kwargs): - if not isinstance(instance, self.model_class): - return # Only init instances of given model (including children) - tracker = self.tracker_class(instance, self.fields, self.field_map) - setattr(instance, self.attname, tracker) - tracker.set_saved_fields() - def __get__(self, instance, owner): if instance is None: return self From c62fe9446d00632661adc1ac4dd1278e8a810694 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 31 Jul 2014 18:16:15 -0600 Subject: [PATCH 010/271] Add Changelog entry for revert of GH-130; bump version to 2.2. --- CHANGES.rst | 11 +++++++++-- model_utils/__init__.py | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index d6ec335..d2a8f5b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,8 +1,15 @@ CHANGES ======= -master (unreleased) -------------------- +2.2 (2014.07.31) +---------------- + +* Revert GH-130, restoring ability to access ``FieldTracker`` changes in + overridden ``save`` methods or ``post_save`` handlers. This reopens GH-83 + (inability to pickle models with ``FieldTracker``) until a solution can be + found that doesn't break behavior otherwise. Thanks Brian May for the + report. Fixes GH-143. + 2.1.1 (2014.07.28) ------------------ diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 2ee5bae..15f1a77 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.2a1' +__version__ = '2.2' From 54d915bd4a66dd4ebc41d2d7fd93bf3b1448ddc3 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 31 Jul 2014 18:57:29 -0600 Subject: [PATCH 011/271] Bump version to 2.3a1. --- CHANGES.rst | 3 +++ model_utils/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index d2a8f5b..04b0d17 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,9 @@ CHANGES ======= +master (unreleased) +------------------- + 2.2 (2014.07.31) ---------------- diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 15f1a77..6161543 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.2' +__version__ = '2.3a1' From 041ef6b8388f4b87ffe5733568d05550c61bdd29 Mon Sep 17 00:00:00 2001 From: bboogaard Date: Tue, 19 Aug 2014 10:20:33 +0200 Subject: [PATCH 012/271] Keep track of deferred fields on model instance Instead of on FieldInstanceTracker instance Signed-off-by: bboogaard --- model_utils/tests/tests.py | 20 +++++++++++++++++--- model_utils/tracker.py | 10 +++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index 38150a8..bbacfd1 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -1485,13 +1485,13 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): self.instance.number = 1 self.instance.save() item = list(self.tracked_class.objects.only('name').all())[0] - self.assertTrue(item.tracker.deferred_fields) + self.assertTrue(item._deferred_fields) self.assertEqual(item.tracker.previous('number'), None) - self.assertTrue('number' in item.tracker.deferred_fields) + self.assertTrue('number' in item._deferred_fields) self.assertEqual(item.number, 1) - self.assertTrue('number' not in item.tracker.deferred_fields) + self.assertTrue('number' not in item._deferred_fields) self.assertEqual(item.tracker.previous('number'), 1) self.assertFalse(item.tracker.has_changed('number')) @@ -1499,6 +1499,20 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): self.assertTrue(item.tracker.has_changed('number')) +class FieldTrackerMultipleInstancesTests(TestCase): + + def test_with_deferred_fields_access_multiple(self): + instances = [ + Tracked.objects.create(pk=1, name='foo', number=1), + Tracked.objects.create(pk=2, name='bar', number=2) + ] + + queryset = Tracked.objects.only('id') + + for instance in queryset: + name = instance.name + + class FieldTrackedModelCustomTests(FieldTrackerTestCase, FieldTrackerCommonTests): diff --git a/model_utils/tracker.py b/model_utils/tracker.py index 6cb4355..be5a7ff 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -32,10 +32,10 @@ class FieldInstanceTracker(object): def current(self, fields=None): """Returns dict of current values for all tracked fields""" if fields is None: - if self.deferred_fields: + if self.instance._deferred_fields: fields = [ field for field in self.fields - if field not in self.deferred_fields + if field not in self.instance._deferred_fields ] else: fields = self.fields @@ -62,7 +62,7 @@ class FieldInstanceTracker(object): ) def init_deferred_fields(self): - self.deferred_fields = [] + self.instance._deferred_fields = [] if not self.instance._deferred: return @@ -70,7 +70,7 @@ class FieldInstanceTracker(object): def __get__(field, instance, owner): data = instance.__dict__ if data.get(field.field_name, field) is field: - self.deferred_fields.remove(field.field_name) + instance._deferred_fields.remove(field.field_name) value = super(DeferredAttributeTracker, field).__get__( instance, owner) self.saved_data[field.field_name] = deepcopy(value) @@ -79,7 +79,7 @@ class FieldInstanceTracker(object): for field in self.fields: field_obj = self.instance.__class__.__dict__.get(field) if isinstance(field_obj, DeferredAttribute): - self.deferred_fields.append(field) + self.instance._deferred_fields.append(field) # Django 1.4 model = None From ac0957e3ec8bafd5a893fd99171972f48b6a6d00 Mon Sep 17 00:00:00 2001 From: bboogaard Date: Tue, 19 Aug 2014 10:42:41 +0200 Subject: [PATCH 013/271] Update AUTHORS.rst and CHANGES.rst --- AUTHORS.rst | 1 + CHANGES.rst | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index bacbd67..2a26eeb 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,6 +1,7 @@ Alejandro Varas Alex Orange Andy Freeland +Bram Boogaard Carl Meyer Curtis Maloney Den Lesnov diff --git a/CHANGES.rst b/CHANGES.rst index 04b0d17..250e036 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +* Keep track of deferred fields on model instance instead of on +FieldInstanceTracker instance. Accessing deferred fields for multiple instances +of a model from the same queryset fails in current release. + + CHANGES ======= From 31170692a54dec9f863e47475b9d7c32eb18e876 Mon Sep 17 00:00:00 2001 From: bboogaard Date: Mon, 1 Sep 2014 09:30:29 +0200 Subject: [PATCH 014/271] Update CHANGES.rst --- CHANGES.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 250e036..e69171f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,14 +1,14 @@ -* Keep track of deferred fields on model instance instead of on -FieldInstanceTracker instance. Accessing deferred fields for multiple instances -of a model from the same queryset fails in current release. - - CHANGES ======= master (unreleased) ------------------- +* Keep track of deferred fields on model instance instead of on +FieldInstanceTracker instance. Accessing deferred fields for multiple instances +of a model from the same queryset fails in current release. + + 2.2 (2014.07.31) ---------------- From 67ec6e4b53654b0a9dc12a88371fb13f5939d00a Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 22 Sep 2014 12:12:39 -0600 Subject: [PATCH 015/271] Update changelog. --- CHANGES.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e69171f..efdde71 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,8 +5,9 @@ master (unreleased) ------------------- * Keep track of deferred fields on model instance instead of on -FieldInstanceTracker instance. Accessing deferred fields for multiple instances -of a model from the same queryset fails in current release. + FieldInstanceTracker instance. Fixes accessing deferred fields for multiple + instances of a model from the same queryset. Thanks Bram Boogaard. Merge of + GH-151. 2.2 (2014.07.31) From 9786672361d21788532ed549f48c6aa3fee0b227 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 22 Sep 2014 12:49:35 -0600 Subject: [PATCH 016/271] Remove dead code branch. --- model_utils/fields.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/model_utils/fields.py b/model_utils/fields.py index 8728d56..10a6290 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -120,8 +120,7 @@ class MonitorField(models.DateTimeField): def deconstruct(self): name, path, args, kwargs = super(MonitorField, self).deconstruct() - if self.monitor is not None: - kwargs['monitor'] = self.monitor + kwargs['monitor'] = self.monitor if self.when is not None: kwargs['when'] = self.when return name, path, args, kwargs From 50caabdd2ede840e4ad3113043d8146c783e7043 Mon Sep 17 00:00:00 2001 From: ad-m Date: Wed, 29 Oct 2014 00:27:10 +0100 Subject: [PATCH 017/271] Fix #156 issue --- model_utils/fields.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/model_utils/fields.py b/model_utils/fields.py index 10a6290..99e1b6f 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -229,6 +229,10 @@ class SplitField(models.TextField): except AttributeError: return value + def deconstruct(self): + name, path, args, kwargs = super(SplitField, self).deconstruct() + kwargs['no_excerpt_field'] = self.add_excerpt_field + return name, path, args, kwargs # allow South to handle these fields smoothly try: From b54d4652a3c90fe622c504b4f42260120d2b6f9d Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 29 Oct 2014 16:20:02 -0600 Subject: [PATCH 018/271] Update AUTHORS/changelog. --- AUTHORS.rst | 1 + CHANGES.rst | 3 +++ 2 files changed, 4 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 2a26eeb..c4a9367 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,3 +1,4 @@ +ad-m Alejandro Varas Alex Orange Andy Freeland diff --git a/CHANGES.rst b/CHANGES.rst index efdde71..b0f75bd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,9 @@ master (unreleased) instances of a model from the same queryset. Thanks Bram Boogaard. Merge of GH-151. +* Fix Django 1.7 migrations compatibility for SplitField. Thanks ad-m. Merge of + GH-157; fixes GH-156. + 2.2 (2014.07.31) ---------------- From 254e5493c60daaa7394532c35842e1e663597c6f Mon Sep 17 00:00:00 2001 From: Dmytro Kyrychuck Date: Tue, 27 Jan 2015 00:34:45 +0200 Subject: [PATCH 019/271] bumped django 1.4.10 to 1.4.18 --- .travis.yml | 6 +++--- tox.ini | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index edaae71..e3ff33f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - 3.3 env: - - DJANGO=Django==1.4.10 SOUTH=1 + - DJANGO=Django==1.4.18 SOUTH=1 - DJANGO=Django==1.5.5 SOUTH=1 - DJANGO=Django==1.6.1 SOUTH=1 - DJANGO=https://github.com/django/django/tarball/master SOUTH=1 @@ -26,9 +26,9 @@ matrix: - python: 2.6 env: DJANGO=https://github.com/django/django/tarball/master SOUTH=1 - python: 3.2 - env: DJANGO=Django==1.4.10 SOUTH=1 + env: DJANGO=Django==1.4.18 SOUTH=1 - python: 3.3 - env: DJANGO=Django==1.4.10 SOUTH=1 + env: DJANGO=Django==1.4.18 SOUTH=1 include: - python: 2.7 env: DJANGO=Django==1.5.5 SOUTH=0 diff --git a/tox.ini b/tox.ini index b754bc7..d6b65ae 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ commands = coverage run -a setup.py test [testenv:py26-1.4] basepython = python2.6 deps = - Django == 1.4.10 + Django == 1.4.18 South == 0.7.6 coverage == 3.6 @@ -35,7 +35,7 @@ deps = [testenv:py27-1.4] basepython = python2.7 deps = - Django == 1.4.10 + Django == 1.4.18 South == 0.8.1 coverage == 3.6 From a0ffbf0b3998d82a921e6b23e777dcff02d2d761 Mon Sep 17 00:00:00 2001 From: Dmytro Kyrychuck Date: Tue, 27 Jan 2015 00:57:40 +0200 Subject: [PATCH 020/271] bumped django 1.5.5 to 1.5.12 --- .travis.yml | 4 ++-- tox.ini | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index e3ff33f..e984ea3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ python: env: - DJANGO=Django==1.4.18 SOUTH=1 - - DJANGO=Django==1.5.5 SOUTH=1 + - DJANGO=Django==1.5.12 SOUTH=1 - DJANGO=Django==1.6.1 SOUTH=1 - DJANGO=https://github.com/django/django/tarball/master SOUTH=1 @@ -31,6 +31,6 @@ matrix: env: DJANGO=Django==1.4.18 SOUTH=1 include: - python: 2.7 - env: DJANGO=Django==1.5.5 SOUTH=0 + env: DJANGO=Django==1.5.12 SOUTH=0 after_success: coveralls diff --git a/tox.ini b/tox.ini index d6b65ae..9befbb6 100644 --- a/tox.ini +++ b/tox.ini @@ -63,13 +63,13 @@ deps = [testenv:py27-1.5-nosouth] basepython = python2.7 deps = - Django == 1.5.5 + Django == 1.5.12 coverage == 3.6 [testenv:py32-1.5] basepython = python3.2 deps = - Django == 1.5.5 + Django == 1.5.12 South == 0.8.1 coverage == 3.6 @@ -90,7 +90,7 @@ deps = [testenv:py33-1.5] basepython = python3.3 deps = - Django == 1.5.5 + Django == 1.5.12 South == 0.8.1 coverage == 3.6 From be55ed742702efbe2de9a9f4ff1e7d5cdea16103 Mon Sep 17 00:00:00 2001 From: Dmytro Kyrychuck Date: Tue, 27 Jan 2015 00:59:10 +0200 Subject: [PATCH 021/271] bumped django 1.6.1 to 1.6.10 --- .travis.yml | 2 +- tox.ini | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index e984ea3..aa58aa0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ python: env: - DJANGO=Django==1.4.18 SOUTH=1 - DJANGO=Django==1.5.12 SOUTH=1 - - DJANGO=Django==1.6.1 SOUTH=1 + - DJANGO=Django==1.6.10 SOUTH=1 - DJANGO=https://github.com/django/django/tarball/master SOUTH=1 install: diff --git a/tox.ini b/tox.ini index 9befbb6..0efa45f 100644 --- a/tox.ini +++ b/tox.ini @@ -49,7 +49,7 @@ deps = [testenv:py27-1.6] basepython = python2.7 deps = - Django == 1.6.1 + Django == 1.6.10 South == 0.8.1 coverage == 3.6 @@ -76,7 +76,7 @@ deps = [testenv:py32-1.6] basepython = python3.2 deps = - Django == 1.6.1 + Django == 1.6.10 South == 0.8.1 coverage == 3.6 @@ -97,7 +97,7 @@ deps = [testenv:py33-1.6] basepython = python3.3 deps = - Django == 1.6.1 + Django == 1.6.10 South == 0.8.1 coverage == 3.6 From 8796f05ae3a2076157b0164e440c248519fe25ca Mon Sep 17 00:00:00 2001 From: Dmytro Kyrychuck Date: Tue, 27 Jan 2015 01:05:29 +0200 Subject: [PATCH 022/271] added django 1.7.3 to build matrix (fixes #152) --- .travis.yml | 3 +++ tox.ini | 24 +++++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index aa58aa0..d613163 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ env: - DJANGO=Django==1.4.18 SOUTH=1 - DJANGO=Django==1.5.12 SOUTH=1 - DJANGO=Django==1.6.10 SOUTH=1 + - DJANGO=Django==1.7.3 SOUTH=0 - DJANGO=https://github.com/django/django/tarball/master SOUTH=1 install: @@ -25,6 +26,8 @@ matrix: exclude: - python: 2.6 env: DJANGO=https://github.com/django/django/tarball/master SOUTH=1 + - python: 2.6 + env: DJANGO=Django==1.7.3 SOUTH=0 - python: 3.2 env: DJANGO=Django==1.4.18 SOUTH=1 - python: 3.3 diff --git a/tox.ini b/tox.ini index 0efa45f..9fd99cb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,9 @@ [tox] envlist = py26-1.4, py26-1.5, py26-1.6, - py27-1.4, py27-1.5, py27-1.6, py27-trunk, py27-1.5-nosouth, - py32-1.5, py32-1.6, py32-trunk, - py33-1.5, py33-1.6, py33-trunk + py27-1.4, py27-1.5, py27-1.6, py27-1.7, py27-trunk, py27-1.5-nosouth, + py32-1.5, py32-1.6, py32-1.7, py32-trunk, + py33-1.5, py33-1.6, py33-1.7, py33-trunk [testenv] deps = @@ -53,6 +53,12 @@ deps = South == 0.8.1 coverage == 3.6 +[testenv:py27-1.7] +basepython = python2.7 +deps = + Django == 1.7.3 + coverage == 3.6 + [testenv:py27-trunk] basepython = python2.7 deps = @@ -80,6 +86,12 @@ deps = South == 0.8.1 coverage == 3.6 +[testenv:py32-1.7] +basepython = python3.2 +deps = + Django == 1.7.3 + coverage == 3.6 + [testenv:py32-trunk] basepython = python3.2 deps = @@ -101,6 +113,12 @@ deps = South == 0.8.1 coverage == 3.6 +[testenv:py33-1.7] +basepython = python3.3 +deps = + Django == 1.7.3 + coverage == 3.6 + [testenv:py33-trunk] basepython = python3.3 deps = From 271d3bfa275795ea32f94f4598c8d43caf57e10b Mon Sep 17 00:00:00 2001 From: Dmytro Kyrychuck Date: Tue, 27 Jan 2015 02:07:23 +0200 Subject: [PATCH 023/271] updated tox.ini to use generative envlist --- tox.ini | 136 +++++++------------------------------------------------- 1 file changed, 16 insertions(+), 120 deletions(-) diff --git a/tox.ini b/tox.ini index 9fd99cb..ac68335 100644 --- a/tox.ini +++ b/tox.ini @@ -1,127 +1,23 @@ [tox] envlist = - py26-1.4, py26-1.5, py26-1.6, - py27-1.4, py27-1.5, py27-1.6, py27-1.7, py27-trunk, py27-1.5-nosouth, - py32-1.5, py32-1.6, py32-1.7, py32-trunk, - py33-1.5, py33-1.6, py33-1.7, py33-trunk + py26-django{14,15,16}, + py27-django14, py27-django15_nosouth, + py{27,32,33}-django{15,16,17,_trunk}, [testenv] +basepython = + py26: python2.6 + py27: python2.7 + py32: python3.2 + py33: python3.3 + deps = - South == 0.8.1 coverage == 3.6 + django14: Django==1.4.18 + django15{,_nosouth}: Django==1.5.12 + django16: Django==1.6.10 + django17: Django==1.7.3 + django_trunk: https://github.com/django/django/tarball/master + django{14,15,16}: South==1.0.2 + commands = coverage run -a setup.py test - -[testenv:py26-1.4] -basepython = python2.6 -deps = - Django == 1.4.18 - South == 0.7.6 - coverage == 3.6 - -[testenv:py26-1.5] -basepython = python2.6 -deps = - Django == 1.5.5 - South == 0.8.1 - coverage == 3.6 - -[testenv:py26-1.6] -basepython = python2.6 -deps = - https://github.com/django/django/tarball/stable/1.6.x - South == 0.8.1 - coverage == 3.6 - -[testenv:py27-1.4] -basepython = python2.7 -deps = - Django == 1.4.18 - South == 0.8.1 - coverage == 3.6 - -[testenv:py27-1.5] -basepython = python2.7 -deps = - Django == 1.5.5 - South == 0.8.1 - coverage == 3.6 - -[testenv:py27-1.6] -basepython = python2.7 -deps = - Django == 1.6.10 - South == 0.8.1 - coverage == 3.6 - -[testenv:py27-1.7] -basepython = python2.7 -deps = - Django == 1.7.3 - coverage == 3.6 - -[testenv:py27-trunk] -basepython = python2.7 -deps = - https://github.com/django/django/tarball/master - South == 0.8.1 - coverage == 3.6 - -[testenv:py27-1.5-nosouth] -basepython = python2.7 -deps = - Django == 1.5.12 - coverage == 3.6 - -[testenv:py32-1.5] -basepython = python3.2 -deps = - Django == 1.5.12 - South == 0.8.1 - coverage == 3.6 - -[testenv:py32-1.6] -basepython = python3.2 -deps = - Django == 1.6.10 - South == 0.8.1 - coverage == 3.6 - -[testenv:py32-1.7] -basepython = python3.2 -deps = - Django == 1.7.3 - coverage == 3.6 - -[testenv:py32-trunk] -basepython = python3.2 -deps = - https://github.com/django/django/tarball/master - South == 0.8.1 - coverage == 3.6 - -[testenv:py33-1.5] -basepython = python3.3 -deps = - Django == 1.5.12 - South == 0.8.1 - coverage == 3.6 - -[testenv:py33-1.6] -basepython = python3.3 -deps = - Django == 1.6.10 - South == 0.8.1 - coverage == 3.6 - -[testenv:py33-1.7] -basepython = python3.3 -deps = - Django == 1.7.3 - coverage == 3.6 - -[testenv:py33-trunk] -basepython = python3.3 -deps = - https://github.com/django/django/tarball/master - South == 0.8.1 - coverage == 3.6 From b03c7fd5b2b395279f811d291ee45fc9b8b2fc7b Mon Sep 17 00:00:00 2001 From: Dmytro Kyrychuck Date: Tue, 27 Jan 2015 03:54:54 +0200 Subject: [PATCH 024/271] travis-ci now runs tox --- .travis.yml | 34 +++++++--------------------------- update_travis_envs.sh | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 27 deletions(-) create mode 100755 update_travis_envs.sh diff --git a/.travis.yml b/.travis.yml index d613163..6335409 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,39 +1,19 @@ language: python -python: - - 2.6 - - 2.7 - - 3.2 - - 3.3 +python: 2.7 env: - - DJANGO=Django==1.4.18 SOUTH=1 - - DJANGO=Django==1.5.12 SOUTH=1 - - DJANGO=Django==1.6.10 SOUTH=1 - - DJANGO=Django==1.7.3 SOUTH=0 - - DJANGO=https://github.com/django/django/tarball/master SOUTH=1 install: - - pip install $DJANGO - - pip install coverage coveralls - - sh -c "if [ '$SOUTH' = '1' ]; then pip install South==0.8.1; fi" + - pip install --upgrade pip setuptools tox virtualenv coveralls script: - - coverage run -a setup.py test - - coverage report + - tox matrix: - exclude: - - python: 2.6 - env: DJANGO=https://github.com/django/django/tarball/master SOUTH=1 - - python: 2.6 - env: DJANGO=Django==1.7.3 SOUTH=0 - - python: 3.2 - env: DJANGO=Django==1.4.18 SOUTH=1 - - python: 3.3 - env: DJANGO=Django==1.4.18 SOUTH=1 - include: - - python: 2.7 - env: DJANGO=Django==1.5.12 SOUTH=0 + allow_failures: + - env: TOXENV=py27-django_trunk + - env: TOXENV=py32-django_trunk + - env: TOXENV=py33-django_trunk after_success: coveralls diff --git a/update_travis_envs.sh b/update_travis_envs.sh new file mode 100755 index 0000000..e008c03 --- /dev/null +++ b/update_travis_envs.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# Updates .travis.yml envs based on tox.ini configuration. + +# Removing old environment list +cp ./.travis.yml ./.travis.yml.bak +cat ./.travis.yml.bak | grep -v "^ - TOXENV=" > ./.travis.yml + +# Inserting envs based on list generated by tox +for env_name in $(tox --showconfig | grep testenv); do + env_name=${env_name#*:}; + env_name=${env_name%]}; + sed -i "/^env:$/a\ +\ \ - TOXENV=${env_name}" ./.travis.yml; +done From c460054180605129eff0f1eef9ddd82d20c1df97 Mon Sep 17 00:00:00 2001 From: Dmytro Kyrychuck Date: Tue, 27 Jan 2015 03:57:12 +0200 Subject: [PATCH 025/271] included tox envs into .travis.yml --- .travis.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6335409..9d55b2b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,23 @@ language: python python: 2.7 env: + - TOXENV=py33-django16 + - TOXENV=py26-django16 + - TOXENV=py26-django14 + - TOXENV=py26-django15 + - TOXENV=py32-django17 + - TOXENV=py32-django16 + - TOXENV=py32-django15 + - TOXENV=py33-django17 + - TOXENV=py33-django15 + - TOXENV=py32-django_trunk + - TOXENV=py27-django17 + - TOXENV=py27-django15 + - TOXENV=py27-django15_nosouth + - TOXENV=py27-django14 + - TOXENV=py27-django16 + - TOXENV=py33-django_trunk + - TOXENV=py27-django_trunk install: - pip install --upgrade pip setuptools tox virtualenv coveralls From 971ae30151803ad77f3d9b3936cf3b076770a3b7 Mon Sep 17 00:00:00 2001 From: Dmytro Kyrychuck Date: Tue, 27 Jan 2015 05:10:38 +0200 Subject: [PATCH 026/271] env list in .travis.yml will now be sorted --- .travis.yml | 22 +++++++++++----------- update_travis_envs.sh | 4 +--- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9d55b2b..285f446 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,23 +3,23 @@ language: python python: 2.7 env: - - TOXENV=py33-django16 - - TOXENV=py26-django16 - TOXENV=py26-django14 - TOXENV=py26-django15 - - TOXENV=py32-django17 - - TOXENV=py32-django16 - - TOXENV=py32-django15 - - TOXENV=py33-django17 - - TOXENV=py33-django15 - - TOXENV=py32-django_trunk - - TOXENV=py27-django17 + - TOXENV=py26-django16 + - TOXENV=py27-django14 - TOXENV=py27-django15 - TOXENV=py27-django15_nosouth - - TOXENV=py27-django14 - TOXENV=py27-django16 - - TOXENV=py33-django_trunk + - TOXENV=py27-django17 - TOXENV=py27-django_trunk + - TOXENV=py32-django15 + - TOXENV=py32-django16 + - TOXENV=py32-django17 + - TOXENV=py32-django_trunk + - TOXENV=py33-django15 + - TOXENV=py33-django16 + - TOXENV=py33-django17 + - TOXENV=py33-django_trunk install: - pip install --upgrade pip setuptools tox virtualenv coveralls diff --git a/update_travis_envs.sh b/update_travis_envs.sh index e008c03..8b5d559 100755 --- a/update_travis_envs.sh +++ b/update_travis_envs.sh @@ -7,9 +7,7 @@ cp ./.travis.yml ./.travis.yml.bak cat ./.travis.yml.bak | grep -v "^ - TOXENV=" > ./.travis.yml # Inserting envs based on list generated by tox -for env_name in $(tox --showconfig | grep testenv); do - env_name=${env_name#*:}; - env_name=${env_name%]}; +for env_name in $(tox --listenvs | sort -r); do sed -i "/^env:$/a\ \ \ - TOXENV=${env_name}" ./.travis.yml; done From 63ba203bda1abd9052809a9b75649aea1846f791 Mon Sep 17 00:00:00 2001 From: Dmytro Kyrychuck Date: Tue, 27 Jan 2015 05:12:47 +0200 Subject: [PATCH 027/271] added django 1.8 envs --- .travis.yml | 3 +++ tox.ini | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 285f446..5cff936 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,14 +11,17 @@ env: - TOXENV=py27-django15_nosouth - TOXENV=py27-django16 - TOXENV=py27-django17 + - TOXENV=py27-django18 - TOXENV=py27-django_trunk - TOXENV=py32-django15 - TOXENV=py32-django16 - TOXENV=py32-django17 + - TOXENV=py32-django18 - TOXENV=py32-django_trunk - TOXENV=py33-django15 - TOXENV=py33-django16 - TOXENV=py33-django17 + - TOXENV=py33-django18 - TOXENV=py33-django_trunk install: diff --git a/tox.ini b/tox.ini index ac68335..d22b9cf 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ envlist = py26-django{14,15,16}, py27-django14, py27-django15_nosouth, - py{27,32,33}-django{15,16,17,_trunk}, + py{27,32,33}-django{15,16,17,18,_trunk}, [testenv] basepython = @@ -17,6 +17,7 @@ deps = django15{,_nosouth}: Django==1.5.12 django16: Django==1.6.10 django17: Django==1.7.3 + django18: Django==1.8a1 django_trunk: https://github.com/django/django/tarball/master django{14,15,16}: South==1.0.2 From 0bb09b419cb0267cd57279ab9658aa673013f5fa Mon Sep 17 00:00:00 2001 From: Dmytro Kyrychuck Date: Tue, 27 Jan 2015 05:25:30 +0200 Subject: [PATCH 028/271] django 1.8 builds is allowed to fail --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 5cff936..6076c58 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,6 +32,9 @@ script: matrix: allow_failures: + - env: TOXENV=py27-django18 + - env: TOXENV=py32-django18 + - env: TOXENV=py33-django18 - env: TOXENV=py27-django_trunk - env: TOXENV=py32-django_trunk - env: TOXENV=py33-django_trunk From 6944bdd218ebf2f6f57b7820ad5a4fe252bd552d Mon Sep 17 00:00:00 2001 From: Dmytro Kyrychuck Date: Tue, 27 Jan 2015 05:34:39 +0200 Subject: [PATCH 029/271] added python3.4 to env list --- .travis.yml | 5 +++++ tox.ini | 2 ++ 2 files changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6076c58..cc17a3a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,9 @@ env: - TOXENV=py33-django17 - TOXENV=py33-django18 - TOXENV=py33-django_trunk + - TOXENV=py34-django17 + - TOXENV=py34-django18 + - TOXENV=py34-django_trunk install: - pip install --upgrade pip setuptools tox virtualenv coveralls @@ -35,8 +38,10 @@ matrix: - env: TOXENV=py27-django18 - env: TOXENV=py32-django18 - env: TOXENV=py33-django18 + - env: TOXENV=py34-django18 - env: TOXENV=py27-django_trunk - env: TOXENV=py32-django_trunk - env: TOXENV=py33-django_trunk + - env: TOXENV=py34-django_trunk after_success: coveralls diff --git a/tox.ini b/tox.ini index d22b9cf..5255cf5 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = py26-django{14,15,16}, py27-django14, py27-django15_nosouth, py{27,32,33}-django{15,16,17,18,_trunk}, + py34-django{17,18,_trunk}, [testenv] basepython = @@ -10,6 +11,7 @@ basepython = py27: python2.7 py32: python3.2 py33: python3.3 + py34: python3.4 deps = coverage == 3.6 From 7012e16cc89be352364082dd2e661fa3d9fc9472 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 26 Jan 2015 22:28:34 -0700 Subject: [PATCH 030/271] Add Dmytro Kyrychuk to AUTHORS file. --- AUTHORS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index c4a9367..dc27bb6 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -6,6 +6,7 @@ Bram Boogaard Carl Meyer Curtis Maloney Den Lesnov +Dmytro Kyrychuk Donald Stufft Douglas Meehan Facundo Gaich From 3110794afc0ccbc4878eb6c7ea53090af44880ba Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 27 Jan 2015 16:43:29 -0700 Subject: [PATCH 031/271] Fix 'add_*_manager' signal handlers for Django 1.8+. --- model_utils/models.py | 49 +++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/model_utils/models.py b/model_utils/models.py index 8030bea..94d2077 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -36,6 +36,7 @@ class TimeFramedModel(models.Model): class Meta: abstract = True + class StatusModel(models.Model): """ An abstract base class model with a ``status`` field that @@ -51,6 +52,7 @@ class StatusModel(models.Model): class Meta: abstract = True + def add_status_query_managers(sender, **kwargs): """ Add a Querymanager for each status item dynamically. @@ -59,16 +61,15 @@ def add_status_query_managers(sender, **kwargs): if not issubclass(sender, StatusModel): return for value, display in getattr(sender, 'STATUS', ()): - try: - sender._meta.get_field(value) - raise ImproperlyConfigured("StatusModel: Model '%s' has a field " - "named '%s' which conflicts with a " - "status of the same name." - % (sender.__name__, value)) - except FieldDoesNotExist: - pass + if _field_exists(sender, value): + raise ImproperlyConfigured( + "StatusModel: Model '%s' has a field named '%s' which " + "conflicts with a status of the same name." + % (sender.__name__, value) + ) sender.add_to_class(value, QueryManager(status=value)) + def add_timeframed_query_manager(sender, **kwargs): """ Add a QueryManager for a specific timeframe. @@ -76,14 +77,12 @@ def add_timeframed_query_manager(sender, **kwargs): """ if not issubclass(sender, TimeFramedModel): return - try: - sender._meta.get_field('timeframed') - raise ImproperlyConfigured("Model '%s' has a field named " - "'timeframed' which conflicts with " - "the TimeFramedModel manager." - % sender.__name__) - except FieldDoesNotExist: - pass + if _field_exists(sender, 'timeframed'): + raise ImproperlyConfigured( + "Model '%s' has a field named 'timeframed' " + "which conflicts with the TimeFramedModel manager." + % sender.__name__ + ) sender.add_to_class('timeframed', QueryManager( (models.Q(start__lte=now) | models.Q(start__isnull=True)) & (models.Q(end__gte=now) | models.Q(end__isnull=True)) @@ -92,3 +91,21 @@ def add_timeframed_query_manager(sender, **kwargs): models.signals.class_prepared.connect(add_status_query_managers) models.signals.class_prepared.connect(add_timeframed_query_manager) + + +def _field_exists(model_class, field_name): + if hasattr(model_class._meta, '_get_fields'): + # Django 1.8+ + field_exists = bool([ + f for f in model_class._meta._get_fields(reverse=False) + if f.name == field_name + ]) + else: + # Django 1.7 and previous + try: + model_class._meta.get_field(field_name) + except FieldDoesNotExist: + field_exists = False + else: + field_exists = True + return field_exists From 3f9b1cfac8a3e316fbb49c03b68a4eb629a67795 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 27 Jan 2015 16:48:06 -0700 Subject: [PATCH 032/271] Fix select_subclasses for Django 1.8. --- model_utils/managers.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/model_utils/managers.py b/model_utils/managers.py index 0028f84..b6496ba 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -8,7 +8,7 @@ from django.core.exceptions import ObjectDoesNotExist try: from django.db.models.constants import LOOKUP_SEP from django.utils.six import string_types -except ImportError: # Django < 1.5 +except ImportError: # Django < 1.5 from django.db.models.sql.constants import LOOKUP_SEP string_types = (basestring,) @@ -53,20 +53,18 @@ class InheritanceQuerySetMixin(object): new_qs.subclasses = subclasses return new_qs - def _clone(self, klass=None, setup=False, **kwargs): for name in ['subclasses', '_annotated']: if hasattr(self, name): kwargs[name] = getattr(self, name) - return super(InheritanceQuerySetMixin, self)._clone(klass, setup, **kwargs) - + return super(InheritanceQuerySetMixin, self)._clone( + klass, setup, **kwargs) def annotate(self, *args, **kwargs): qset = super(InheritanceQuerySetMixin, self).annotate(*args, **kwargs) qset._annotated = [a.default_alias for a in args] + list(kwargs.keys()) return qset - def iterator(self): iter = super(InheritanceQuerySetMixin, self).iterator() if getattr(self, 'subclasses', False): @@ -95,7 +93,6 @@ class InheritanceQuerySetMixin(object): for obj in iter: yield obj - def _get_subclasses_recurse(self, model, levels=None): """ Given a Model class, find all related objects, exploring children @@ -115,11 +112,11 @@ class InheritanceQuerySetMixin(object): if levels or levels is None: for subclass in self._get_subclasses_recurse( rel.field.model, levels=levels): - subclasses.append(rel.get_accessor_name() + LOOKUP_SEP + subclass) + subclasses.append( + rel.get_accessor_name() + LOOKUP_SEP + subclass) subclasses.append(rel.get_accessor_name()) return subclasses - def _get_ancestors_path(self, model, levels=None): """ Serves as an opposite to _get_subclasses_recurse, instead walking from @@ -127,23 +124,27 @@ class InheritanceQuerySetMixin(object): select_related string backwards. """ if not issubclass(model, self.model): - raise ValueError("%r is not a subclass of %r" % (model, self.model)) + raise ValueError( + "%r is not a subclass of %r" % (model, self.model)) ancestry = [] # should be a OneToOneField or None - parent = model._meta.get_ancestor_link(self.model) + parent_link = model._meta.get_ancestor_link(self.model) if levels: levels -= 1 - while parent is not None: - ancestry.insert(0, parent.related.get_accessor_name()) + while parent_link is not None: + ancestry.insert(0, parent_link.related.get_accessor_name()) if levels or levels is None: - parent = parent.related.parent_model._meta.get_ancestor_link( + if django.VERSION < (1, 8): + parent_model = parent_link.related.parent_model + else: + parent_model = parent_link.related.model + parent_link = parent_model._meta.get_ancestor_link( self.model) else: - parent = None + parent_link = None return LOOKUP_SEP.join(ancestry) - def _get_sub_obj_recurse(self, obj, s): rel, _, s = s.partition(LOOKUP_SEP) try: @@ -170,6 +171,7 @@ class InheritanceQuerySetMixin(object): levels = 1 return levels + class InheritanceManagerMixin(object): use_for_related_fields = True @@ -188,6 +190,7 @@ class InheritanceManagerMixin(object): class InheritanceQuerySet(InheritanceQuerySetMixin, QuerySet): pass + class InheritanceManager(InheritanceManagerMixin, models.Manager): pass @@ -271,7 +274,8 @@ class PassThroughManagerMixin(object): @classmethod def for_queryset_class(cls, queryset_cls): - return create_pass_through_manager_for_queryset_class(cls, queryset_cls) + return create_pass_through_manager_for_queryset_class( + cls, queryset_cls) class PassThroughManager(PassThroughManagerMixin, models.Manager): From fce5b4391d496711ea86088ec83f0bfd45fe7ce4 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 27 Jan 2015 16:49:16 -0700 Subject: [PATCH 033/271] Remove allow-failures from Travis config. --- .travis.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index cc17a3a..c2aa337 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,15 +33,4 @@ install: script: - tox -matrix: - allow_failures: - - env: TOXENV=py27-django18 - - env: TOXENV=py32-django18 - - env: TOXENV=py33-django18 - - env: TOXENV=py34-django18 - - env: TOXENV=py27-django_trunk - - env: TOXENV=py32-django_trunk - - env: TOXENV=py33-django_trunk - - env: TOXENV=py34-django_trunk - after_success: coveralls From a4680a32c65f9cbea35428834723a4dea652b5ee Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 27 Jan 2015 16:59:22 -0700 Subject: [PATCH 034/271] Add Django trunk builds back to allowed-failures. --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index c2aa337..b24d579 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,4 +33,11 @@ install: script: - tox +matrix: + allow_failures: + - env: TOXENV=py27-django_trunk + - env: TOXENV=py32-django_trunk + - env: TOXENV=py33-django_trunk + - env: TOXENV=py34-django_trunk + after_success: coveralls From a1088dba52eeca1926c6e091cbfa1fa815388967 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 28 Jan 2015 10:59:20 -0700 Subject: [PATCH 035/271] Simpler cross-version implementation of _field_exists. --- model_utils/models.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/model_utils/models.py b/model_utils/models.py index 94d2077..9acc077 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -94,18 +94,4 @@ models.signals.class_prepared.connect(add_timeframed_query_manager) def _field_exists(model_class, field_name): - if hasattr(model_class._meta, '_get_fields'): - # Django 1.8+ - field_exists = bool([ - f for f in model_class._meta._get_fields(reverse=False) - if f.name == field_name - ]) - else: - # Django 1.7 and previous - try: - model_class._meta.get_field(field_name) - except FieldDoesNotExist: - field_exists = False - else: - field_exists = True - return field_exists + return field_name in {f.attname for f in model_class._meta.local_fields} From d797996d13a871328bada34aef8ad25534e0058f Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 28 Jan 2015 12:17:34 -0700 Subject: [PATCH 036/271] Fix Python 2.6 compatibility. --- model_utils/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model_utils/models.py b/model_utils/models.py index 9acc077..f343dfb 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -94,4 +94,4 @@ models.signals.class_prepared.connect(add_timeframed_query_manager) def _field_exists(model_class, field_name): - return field_name in {f.attname for f in model_class._meta.local_fields} + return field_name in [f.attname for f in model_class._meta.local_fields] From 608028aba8c3a416d9a49c72ced63aba0ce31bea Mon Sep 17 00:00:00 2001 From: Sergey Zherevchuk Date: Thu, 18 Jun 2015 11:11:28 +0300 Subject: [PATCH 037/271] Fix #169 issue Hardcoding no_excerpt_field field in deconstruct() method of SplitField class --- model_utils/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model_utils/fields.py b/model_utils/fields.py index 99e1b6f..74c44b2 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -231,7 +231,7 @@ class SplitField(models.TextField): def deconstruct(self): name, path, args, kwargs = super(SplitField, self).deconstruct() - kwargs['no_excerpt_field'] = self.add_excerpt_field + kwargs['no_excerpt_field'] = True return name, path, args, kwargs # allow South to handle these fields smoothly From 15275b3a9f3d5aaf2b69727f14f200b27f3ba75e Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Sun, 28 Jun 2015 18:46:01 -0700 Subject: [PATCH 038/271] Use shields.io for PyPI badge pypipins is down: badges/pypipins#37 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0f76cf9..b7ddd73 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,7 @@ django-model-utils :target: http://travis-ci.org/carljm/django-model-utils .. image:: https://coveralls.io/repos/carljm/django-model-utils/badge.png?branch=master :target: https://coveralls.io/r/carljm/django-model-utils -.. image:: https://pypip.in/v/django-model-utils/badge.png +.. image:: https://img.shields.io/pypi/v/django-model-utils.svg :target: https://crate.io/packages/django-model-utils Django model mixins and utilities. From 8015b86237b17de75a188c1c1d8f402e64a9ce9a Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Sun, 28 Jun 2015 18:49:36 -0700 Subject: [PATCH 039/271] :sparkles: Happy New Year! :sparkles: --- LICENSE.txt | 2 +- docs/conf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index ab400d7..0eadf47 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2009-2013, Carl Meyer and contributors +Copyright (c) 2009-2015, Carl Meyer and contributors All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/docs/conf.py b/docs/conf.py index d79adff..9f0c4e7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,7 +41,7 @@ master_doc = 'index' # General information about the project. project = u'django-model-utils' -copyright = u'2013, Carl Meyer' +copyright = u'2015, Carl Meyer' parent_dir = os.path.dirname(os.path.dirname(__file__)) From 0ddff705c67f675ea8ddb43a933033b168d53e11 Mon Sep 17 00:00:00 2001 From: Philipp Steinhardt Date: Wed, 1 Jul 2015 14:16:03 +0200 Subject: [PATCH 040/271] * compile django messege catalogs during setup and install them * add compiled django translations to gitignore --- .gitignore | 1 + setup.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c933d31..a09405a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ Django-*.egg htmlcov/ docs/_build/ .idea/ +*.mo diff --git a/setup.py b/setup.py index 25fca37..b34b241 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,8 @@ from os.path import join from setuptools import setup, find_packages +from setuptools.command.install_lib import install_lib as _install_lib +from distutils.command.build import build as _build +from distutils.cmd import Command long_description = (open('README.rst').read() + @@ -14,6 +17,46 @@ def get_version(): return line.split('=')[1].strip().strip('"\'') +class compile_translations(Command): + """command tries to compile messages via django compilemessages, does not + interrupt setup if gettext is not installed""" + + description = 'compile message catalogs to MO files via django compilemessages' + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + import os + import sys + + from django.core.management import execute_from_command_line, CommandError + + curdir = os.getcwd() + os.chdir(os.path.realpath('model_utils')) + + try: + execute_from_command_line(['django-admin', 'compilemessages']) + except CommandError: + # raised if gettext pkg is not installed + pass + finally: + os.chdir(curdir) + + +class build(_build): + sub_commands = [('compile_translations', None)] + _build.sub_commands + + +class install_lib(_install_lib): + def run(self): + self.run_command('compile_translations') + _install_lib.run(self) + setup( name='django-model-utils', version=get_version(), @@ -40,5 +83,12 @@ setup( ], zip_safe=False, tests_require=["Django>=1.4.2"], - test_suite='runtests.runtests' + test_suite='runtests.runtests', + setup_requires=['Django>=1.4.2'], + include_package_data=True, + package_data = { + 'model_utils': ['locale/*/LC_MESSAGES/django.po','locale/*/LC_MESSAGES/django.mo'], + }, + cmdclass={'build': build, 'install_lib': install_lib, + 'compile_translations': compile_translations} ) From 5218b483c564bec8135705cf074bf318a0994364 Mon Sep 17 00:00:00 2001 From: Philipp Steinhardt Date: Wed, 1 Jul 2015 14:16:40 +0200 Subject: [PATCH 041/271] * add german translations --- model_utils/locale/de/LC_MESSAGES/django.po | 55 +++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 model_utils/locale/de/LC_MESSAGES/django.po diff --git a/model_utils/locale/de/LC_MESSAGES/django.po b/model_utils/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..dc31374 --- /dev/null +++ b/model_utils/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,55 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-07-01 10:03+0200\n" +"PO-Revision-Date: 2015-07-01 10:12+0200\n" +"Language: de\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" +"Last-Translator: \n" +"Language-Team: \n" +"X-Generator: Poedit 1.8.2\n" + +#: .\models.py:20 +msgid "created" +msgstr "erstellt" + +#: .\models.py:21 +msgid "modified" +msgstr "bearbeitet" + +#: .\models.py:33 +msgid "start" +msgstr "Beginn" + +#: .\models.py:34 +msgid "end" +msgstr "Ende" + +#: .\models.py:49 +msgid "status" +msgstr "Status" + +#: .\models.py:50 +msgid "status changed" +msgstr "Status geändert" + +#: .\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 "gelöscht" + +#: .\tests\models.py:108 .\tests\models.py:117 .\tests\models.py:126 +msgid "on hold" +msgstr "wartend" From 3c54c847b8b24a9e3a85b9e5cdf3428562d913dd Mon Sep 17 00:00:00 2001 From: Philipp Steinhardt Date: Thu, 2 Jul 2015 13:41:26 +0200 Subject: [PATCH 042/271] * include .po message catalogs in source distributions * remove include_package_data from setup --- MANIFEST.in | 1 + setup.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index ddc2005..48f0ac9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,3 +4,4 @@ include LICENSE.txt include MANIFEST.in include README.rst include TODO.rst +locale/*/LC_MESSAGES/django.po \ No newline at end of file diff --git a/setup.py b/setup.py index b34b241..40d8e9b 100644 --- a/setup.py +++ b/setup.py @@ -85,7 +85,6 @@ setup( tests_require=["Django>=1.4.2"], test_suite='runtests.runtests', setup_requires=['Django>=1.4.2'], - include_package_data=True, package_data = { 'model_utils': ['locale/*/LC_MESSAGES/django.po','locale/*/LC_MESSAGES/django.mo'], }, From 1bfee88c069955ff5e094723e9393c7ad2ae8e68 Mon Sep 17 00:00:00 2001 From: Philipp Steinhardt Date: Thu, 2 Jul 2015 13:53:39 +0200 Subject: [PATCH 043/271] * clean up po file header --- model_utils/locale/de/LC_MESSAGES/django.po | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/model_utils/locale/de/LC_MESSAGES/django.po b/model_utils/locale/de/LC_MESSAGES/django.po index dc31374..89c7426 100644 --- a/model_utils/locale/de/LC_MESSAGES/django.po +++ b/model_utils/locale/de/LC_MESSAGES/django.po @@ -1,11 +1,10 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. +# This file is distributed under the same license as the django-model-utils package. # +# Translators: +# Philipp Steinhardt , 2015. msgid "" msgstr "" -"Project-Id-Version: \n" +"Project-Id-Version: django-model-utils\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2015-07-01 10:03+0200\n" "PO-Revision-Date: 2015-07-01 10:12+0200\n" @@ -14,9 +13,8 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"Last-Translator: \n" +"Last-Translator: Philipp Steinhardt \n" "Language-Team: \n" -"X-Generator: Poedit 1.8.2\n" #: .\models.py:20 msgid "created" From 1545e3179c704f86caad6e74d547ffe7386b9ada Mon Sep 17 00:00:00 2001 From: Philipp Steinhardt Date: Thu, 2 Jul 2015 14:27:54 +0200 Subject: [PATCH 044/271] * add translations paragraph to CONTRIBUTING.rst --- CONTRIBUTING.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a789156..4992ade 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -22,11 +22,29 @@ When creating a pull request, try to: - Note important changes in the `CHANGES`_ file - Update the documentation if needed - Add yourself to the `AUTHORS`_ file +- If you have added or changed translation strings, update translations + of languages you are able to do so. Also mention that translations need + to be updated in your pull request and commit message. .. _AUTHORS: AUTHORS.rst .. _CHANGES: CHANGES.rst +Translations +------------ + +If you are able to provide translations for a new language or to update an +existing translation file, make sure to run makemessages beforehand:: + + python django-admin.py makemessages -l ISO_LANGUAGE_CODE + +This command will collect all translation strings from the source directory +and create or update the translation file for the given language. Now open the +translation file (.po) with a text-editor and start editing. +After you finished editing add yourself to the list of translators. +If you have created a new translation, make sure to copy the header from one +of the existing translation files. + Testing ------- From 8bd2f8bcda15d5456cc6bc6ad04d525c5019a1b6 Mon Sep 17 00:00:00 2001 From: Philipp Steinhardt Date: Thu, 2 Jul 2015 14:30:09 +0200 Subject: [PATCH 045/271] * update AUTHORS.rst and CHANGES.rst --- AUTHORS.rst | 1 + CHANGES.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index dc27bb6..b25a3cf 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -24,6 +24,7 @@ Michael van Tellingen Mikhail Silonov Patryk Zawadzki Paul McLanahan +Philipp Steinhardt Rinat Shigapov Rodney Folz rsenkbeil diff --git a/CHANGES.rst b/CHANGES.rst index b0f75bd..86eccf5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,8 @@ master (unreleased) * Fix Django 1.7 migrations compatibility for SplitField. Thanks ad-m. Merge of GH-157; fixes GH-156. + +* Add German translations. 2.2 (2014.07.31) From b09fa1e14e31224b7148ff6068a670521ba7fe55 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 17 Jul 2015 11:37:59 -0600 Subject: [PATCH 046/271] Add changelog note about Django 1.8 compat. --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 86eccf5..f65f16d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,9 +11,11 @@ master (unreleased) * Fix Django 1.7 migrations compatibility for SplitField. Thanks ad-m. Merge of GH-157; fixes GH-156. - + * Add German translations. +* Django 1.8 compatibility. + 2.2 (2014.07.31) ---------------- From b1c183d475bde8d9e64320297050069b250968b0 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 17 Jul 2015 11:38:39 -0600 Subject: [PATCH 047/271] Bump version to 2.3. --- CHANGES.rst | 4 ++-- model_utils/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index f65f16d..01dc0a8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,8 +1,8 @@ CHANGES ======= -master (unreleased) -------------------- +2.3 (2015.07.17) +---------------- * Keep track of deferred fields on model instance instead of on FieldInstanceTracker instance. Fixes accessing deferred fields for multiple diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 6161543..eff53a7 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.3a1' +__version__ = '2.3' From ab4a2b4ce452fc7eb2987b28d8cb06b15d23b941 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 17 Jul 2015 13:02:07 -0600 Subject: [PATCH 048/271] Bump version to 2.4a1. --- .gitignore | 1 + CHANGES.rst | 4 ++++ model_utils/__init__.py | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a09405a..ebd0ff6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ htmlcov/ docs/_build/ .idea/ *.mo +.eggs/ diff --git a/CHANGES.rst b/CHANGES.rst index 01dc0a8..fbc92fc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,10 @@ CHANGES ======= +master (unreleased) +------------------- + + 2.3 (2015.07.17) ---------------- diff --git a/model_utils/__init__.py b/model_utils/__init__.py index eff53a7..a09be7d 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.3' +__version__ = '2.4a1' From b2149c8e73d0f65e7bb21b9707051682aa8fcd6c Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 20 Jul 2015 09:56:07 -0600 Subject: [PATCH 049/271] Remove translations automation in setup.py. Fixes GH-178, GH-179. --- CHANGES.rst | 3 +++ setup.py | 53 ++++------------------------------------------------- 2 files changed, 7 insertions(+), 49 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index fbc92fc..736756b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,9 @@ CHANGES master (unreleased) ------------------- +* Remove all translation-related automation in `setup.py`. Fixes GH-178 and + GH-179. Thanks Joe Weiss, Matt Molyneaux, and others for the reports. + 2.3 (2015.07.17) ---------------- diff --git a/setup.py b/setup.py index 40d8e9b..a1b49b6 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,5 @@ from os.path import join from setuptools import setup, find_packages -from setuptools.command.install_lib import install_lib as _install_lib -from distutils.command.build import build as _build -from distutils.cmd import Command long_description = (open('README.rst').read() + @@ -16,47 +13,6 @@ def get_version(): if line.startswith('__version__ ='): return line.split('=')[1].strip().strip('"\'') - -class compile_translations(Command): - """command tries to compile messages via django compilemessages, does not - interrupt setup if gettext is not installed""" - - description = 'compile message catalogs to MO files via django compilemessages' - user_options = [] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - import os - import sys - - from django.core.management import execute_from_command_line, CommandError - - curdir = os.getcwd() - os.chdir(os.path.realpath('model_utils')) - - try: - execute_from_command_line(['django-admin', 'compilemessages']) - except CommandError: - # raised if gettext pkg is not installed - pass - finally: - os.chdir(curdir) - - -class build(_build): - sub_commands = [('compile_translations', None)] + _build.sub_commands - - -class install_lib(_install_lib): - def run(self): - self.run_command('compile_translations') - _install_lib.run(self) - setup( name='django-model-utils', version=get_version(), @@ -84,10 +40,9 @@ setup( zip_safe=False, tests_require=["Django>=1.4.2"], test_suite='runtests.runtests', - setup_requires=['Django>=1.4.2'], - package_data = { - 'model_utils': ['locale/*/LC_MESSAGES/django.po','locale/*/LC_MESSAGES/django.mo'], + package_data={ + 'model_utils': [ + 'locale/*/LC_MESSAGES/django.po','locale/*/LC_MESSAGES/django.mo' + ], }, - cmdclass={'build': build, 'install_lib': install_lib, - 'compile_translations': compile_translations} ) From 114f4fe2281537a5b48234e052733a8fa1692dcf Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 20 Jul 2015 10:20:11 -0600 Subject: [PATCH 050/271] Add script and makefile tasks for making and compiling messages; un-gitignore .mo files. --- .gitignore | 1 - CONTRIBUTING.rst | 10 +++-- Makefile | 6 +++ model_utils/locale/de/LC_MESSAGES/django.mo | Bin 0 -> 760 bytes model_utils/locale/de/LC_MESSAGES/django.po | 24 +++++------ translations.py | 45 ++++++++++++++++++++ 6 files changed, 69 insertions(+), 17 deletions(-) create mode 100644 model_utils/locale/de/LC_MESSAGES/django.mo create mode 100755 translations.py diff --git a/.gitignore b/.gitignore index ebd0ff6..5f1c259 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,4 @@ Django-*.egg htmlcov/ docs/_build/ .idea/ -*.mo .eggs/ diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 4992ade..6b2b848 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -22,9 +22,11 @@ When creating a pull request, try to: - Note important changes in the `CHANGES`_ file - Update the documentation if needed - Add yourself to the `AUTHORS`_ file -- If you have added or changed translation strings, update translations - of languages you are able to do so. Also mention that translations need - to be updated in your pull request and commit message. +- If you have added or changed translated strings, run ``make messages`` to + update the ``.po`` translation files, and update translations for any + languages you know. Then run ``make compilemessages`` to compile the ``.mo`` + files. If your pull request leaves some translations incomplete, please + mention that in the pull request and commit message. .. _AUTHORS: AUTHORS.rst .. _CHANGES: CHANGES.rst @@ -37,7 +39,7 @@ If you are able to provide translations for a new language or to update an existing translation file, make sure to run makemessages beforehand:: python django-admin.py makemessages -l ISO_LANGUAGE_CODE - + This command will collect all translation strings from the source directory and create or update the translation file for the given language. Now open the translation file (.po) with a text-editor and start editing. diff --git a/Makefile b/Makefile index c736206..4785478 100644 --- a/Makefile +++ b/Makefile @@ -13,3 +13,9 @@ docs: documentation documentation: python setup.py build_sphinx + +messages: + python translations.py make + +compilemessages: + python translations.py compile diff --git a/model_utils/locale/de/LC_MESSAGES/django.mo b/model_utils/locale/de/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..7b80928ef794836df63af2149725a5f84bdaa149 GIT binary patch literal 760 zcmZ9J&ubJh6vv}ht%HgP-V`EU{6Xr>g0{}OBCT5}xVW^d_w3|#HZ_|JNw(TQ#Q(s9 zCy#pZA_eary@)rjdh_TvJ8KIDzVpc=-}mMv@B8JYHw-I-u0jU70DXee`URbVenV%W zKMS%GjGe-M8C(X>gX#PVcpAJ0ro0>AMesIgp$AYpe|NIYdk>yN{1IFNKZB{4FJP+o z6-;%$E%*bxg!m_jQb#M*O!+A%`BP0;<~^o*smJ+}Rn+^$Do-+!Vu}nUl2vT#l-|_{ zoaGZ|6bn)Mn7(lq7U0!H+8UmrcY9Ptv8BWd@`QI!k{5kj;fZZs z^0l#!+0e|qG-A{FX(+jE=3(e{y1d=?Zd!i5$BSFtjLDZ68^c3KGA6Q`%;ASnO(N@C iMQlvck4Z<0>=j0qhPh6)wwCR*!)w87I(!R1hWZyZioON_ literal 0 HcmV?d00001 diff --git a/model_utils/locale/de/LC_MESSAGES/django.po b/model_utils/locale/de/LC_MESSAGES/django.po index 89c7426..342b3cf 100644 --- a/model_utils/locale/de/LC_MESSAGES/django.po +++ b/model_utils/locale/de/LC_MESSAGES/django.po @@ -6,48 +6,48 @@ msgid "" msgstr "" "Project-Id-Version: django-model-utils\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-07-01 10:03+0200\n" +"POT-Creation-Date: 2015-07-20 10:17-0600\n" "PO-Revision-Date: 2015-07-01 10:12+0200\n" +"Last-Translator: Philipp Steinhardt \n" +"Language-Team: \n" "Language: de\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" -"Last-Translator: Philipp Steinhardt \n" -"Language-Team: \n" -#: .\models.py:20 +#: models.py:20 msgid "created" msgstr "erstellt" -#: .\models.py:21 +#: models.py:21 msgid "modified" msgstr "bearbeitet" -#: .\models.py:33 +#: models.py:33 msgid "start" msgstr "Beginn" -#: .\models.py:34 +#: models.py:34 msgid "end" msgstr "Ende" -#: .\models.py:49 +#: models.py:49 msgid "status" msgstr "Status" -#: .\models.py:50 +#: models.py:50 msgid "status changed" msgstr "Status geändert" -#: .\tests\models.py:106 .\tests\models.py:115 .\tests\models.py:124 +#: 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 +#: tests/models.py:107 tests/models.py:116 tests/models.py:125 msgid "deleted" msgstr "gelöscht" -#: .\tests\models.py:108 .\tests\models.py:117 .\tests\models.py:126 +#: tests/models.py:108 tests/models.py:117 tests/models.py:126 msgid "on hold" msgstr "wartend" diff --git a/translations.py b/translations.py new file mode 100755 index 0000000..58b107f --- /dev/null +++ b/translations.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python + +import os +import sys + +from django.conf import settings +import django + + +DEFAULT_SETTINGS = dict( + INSTALLED_APPS=( + 'model_utils', + 'model_utils.tests', + ), + DATABASES={ + "default": { + "ENGINE": "django.db.backends.sqlite3" + } + }, + SILENCED_SYSTEM_CHECKS=["1_7.W001"], + ) + + +def run(command): + if not settings.configured: + settings.configure(**DEFAULT_SETTINGS) + + # Compatibility with Django 1.7's stricter initialization + if hasattr(django, 'setup'): + django.setup() + + parent = os.path.dirname(os.path.abspath(__file__)) + appdir = os.path.join(parent, 'model_utils') + os.chdir(appdir) + + from django.core.management import call_command + + call_command('%smessages' % command) + + +if __name__ == '__main__': + if (len(sys.argv)) < 2 or (sys.argv[1] not in {'make', 'compile'}): + print("Run `translations.py make` or `translations.py compile`.") + sys.exit(1) + run(sys.argv[1]) From 443108c7ad2d4821392a6a5999006ec53d1f2462 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 20 Jul 2015 10:22:27 -0600 Subject: [PATCH 051/271] Set version for 2.3.1 release. --- CHANGES.rst | 4 ++-- model_utils/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 736756b..1fc9a62 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,8 +1,8 @@ CHANGES ======= -master (unreleased) -------------------- +2.3.1 (2015-07-20) +------------------ * Remove all translation-related automation in `setup.py`. Fixes GH-178 and GH-179. Thanks Joe Weiss, Matt Molyneaux, and others for the reports. diff --git a/model_utils/__init__.py b/model_utils/__init__.py index a09be7d..272f4ca 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.4a1' +__version__ = '2.3.1' From 165e0ec495903c2cb00774eb3d473c34226d8583 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 20 Jul 2015 10:32:18 -0600 Subject: [PATCH 052/271] Bump version for 2.4 development. --- CHANGES.rst | 3 +++ model_utils/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1fc9a62..0c20f13 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,9 @@ CHANGES ======= +master (unreleased) +------------------- + 2.3.1 (2015-07-20) ------------------ diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 272f4ca..cb60d23 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.3.1' +__version__ = '2.4a2' From 665fc04b7e4a237d53e73c8cfe59fb0847cbd656 Mon Sep 17 00:00:00 2001 From: jarekwg Date: Wed, 28 Oct 2015 23:47:33 +1100 Subject: [PATCH 053/271] Must use the 'Now' database function in django>=1.9 --- model_utils/models.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/model_utils/models.py b/model_utils/models.py index f343dfb..142b77c 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -1,10 +1,15 @@ from __future__ import unicode_literals +import django from django.db import models from django.utils.translation import ugettext_lazy as _ from django.db.models.fields import FieldDoesNotExist from django.core.exceptions import ImproperlyConfigured -from django.utils.timezone import now +if django.VERSION >= (1, 9, 0): + from django.db.models.functions import Now + now = Now() +else: + from django.utils.timezone import now from model_utils.managers import QueryManager from model_utils.fields import AutoCreatedField, AutoLastModifiedField, \ From 7a33e14f4b31f031cff2419aa00a9f9473a74aa8 Mon Sep 17 00:00:00 2001 From: jarekwg Date: Thu, 29 Oct 2015 00:02:46 +1100 Subject: [PATCH 054/271] Get StatusFields working --- model_utils/fields.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/model_utils/fields.py b/model_utils/fields.py index 74c44b2..805c707 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import django from django.db import models from django.conf import settings from django.utils.encoding import python_2_unicode_compatible @@ -59,6 +60,8 @@ class StatusField(models.CharField): "To use StatusField, the model '%s' must have a %s choices class attribute." \ % (sender.__name__, self.choices_name) self._choices = getattr(sender, self.choices_name) + if django.VERSION >= (1, 9, 0): + self.choices = self._choices if not self.has_default(): self.default = tuple(getattr(sender, self.choices_name))[0][0] # set first as default @@ -68,6 +71,8 @@ class StatusField(models.CharField): # the STATUS class attr being available), but we need to set some dummy # choices now so the super method will add the get_FOO_display method self._choices = [(0, 'dummy')] + if django.VERSION >= (1, 9, 0): + self.choices = self._choices super(StatusField, self).contribute_to_class(cls, name) def deconstruct(self): From 2824ec2e4875ab6530caa440034959d51d5b9830 Mon Sep 17 00:00:00 2001 From: jarekwg Date: Thu, 29 Oct 2015 00:10:28 +1100 Subject: [PATCH 055/271] Remove PassThroughManager As of Django 1.7, QuerySet.as_manager() achieves the same result. --- model_utils/managers.py | 88 ---------------------------- model_utils/tests/models.py | 72 +---------------------- model_utils/tests/tests.py | 112 +----------------------------------- 3 files changed, 2 insertions(+), 270 deletions(-) diff --git a/model_utils/managers.py b/model_utils/managers.py index b6496ba..8e95318 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -224,91 +224,3 @@ class QueryManagerMixin(object): class QueryManager(QueryManagerMixin, models.Manager): pass - - -class PassThroughManagerMixin(object): - """ - A mixin that enables you to call custom QuerySet methods from your manager. - """ - - # pickling causes recursion errors - _deny_methods = ['__getstate__', '__setstate__', '__getinitargs__', - '__getnewargs__', '__copy__', '__deepcopy__', '_db', - '__slots__'] - - def __init__(self, queryset_cls=None): - self._queryset_cls = queryset_cls - super(PassThroughManagerMixin, self).__init__() - - def __getattr__(self, name): - if name in self._deny_methods: - raise AttributeError(name) - if django.VERSION < (1, 6, 0): - return getattr(self.get_query_set(), name) - return getattr(self.get_queryset(), name) - - def __dir__(self): - """ - Allow introspection via dir() and ipythonesque tab-discovery. - - We do dir(type(self)) because to do dir(self) would be a recursion - error. - We call dir(self.get_query_set()) because it is possible that the - queryset returned by get_query_set() is interesting, even if - self._queryset_cls is None. - """ - my_values = frozenset(dir(type(self))) - my_values |= frozenset(dir(self.get_query_set())) - return list(my_values) - - def get_queryset(self): - try: - qs = super(PassThroughManagerMixin, self).get_queryset() - except AttributeError: - qs = super(PassThroughManagerMixin, self).get_query_set() - if self._queryset_cls is not None: - qs = qs._clone(klass=self._queryset_cls) - return qs - - get_query_set = get_queryset - - @classmethod - def for_queryset_class(cls, queryset_cls): - return create_pass_through_manager_for_queryset_class( - cls, queryset_cls) - - -class PassThroughManager(PassThroughManagerMixin, models.Manager): - """ - Inherit from this Manager to enable you to call any methods from your - custom QuerySet class from your manager. Simply define your QuerySet - class, and return an instance of it from your manager's `get_queryset` - method. - - Alternately, if you don't need any extra methods on your manager that - aren't on your QuerySet, then just pass your QuerySet class to the - ``for_queryset_class`` class method. - - class PostQuerySet(QuerySet): - def enabled(self): - return self.filter(disabled=False) - - class Post(models.Model): - objects = PassThroughManager.for_queryset_class(PostQuerySet)() - - """ - pass - - -def create_pass_through_manager_for_queryset_class(base, queryset_cls): - class _PassThroughManager(base): - def __init__(self, *args, **kwargs): - return super(_PassThroughManager, self).__init__(*args, **kwargs) - - def get_queryset(self): - qs = super(_PassThroughManager, self).get_queryset() - return qs._clone(klass=queryset_cls) - - get_query_set = get_queryset - - return _PassThroughManager diff --git a/model_utils/tests/models.py b/model_utils/tests/models.py index ea46d0f..b1903ed 100644 --- a/model_utils/tests/models.py +++ b/model_utils/tests/models.py @@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _ from model_utils.models import TimeStampedModel, StatusModel, TimeFramedModel from model_utils.tracker import FieldTracker, ModelTracker -from model_utils.managers import QueryManager, InheritanceManager, PassThroughManager +from model_utils.managers import QueryManager, InheritanceManager from model_utils.fields import SplitField, MonitorField, StatusField from model_utils.tests.fields import MutableField from model_utils import Choices @@ -201,76 +201,6 @@ class FeaturedManager(models.Manager): get_query_set = get_queryset -class DudeQuerySet(models.query.QuerySet): - def abiding(self): - return self.filter(abides=True) - - def rug_positive(self): - return self.filter(has_rug=True) - - def rug_negative(self): - return self.filter(has_rug=False) - - def by_name(self, name): - return self.filter(name__iexact=name) - - - -class AbidingManager(PassThroughManager): - def get_queryset(self): - return DudeQuerySet(self.model).abiding() - - get_query_set = get_queryset - - def get_stats(self): - return { - "abiding_count": self.count(), - "rug_count": self.rug_positive().count(), - } - - - -class Dude(models.Model): - abides = models.BooleanField(default=True) - name = models.CharField(max_length=20) - has_rug = models.BooleanField(default=False) - - objects = PassThroughManager(DudeQuerySet) - abiders = AbidingManager() - - -class Car(models.Model): - name = models.CharField(max_length=20) - owner = models.ForeignKey(Dude, related_name='cars_owned') - - objects = PassThroughManager(DudeQuerySet) - - -class SpotManager(PassThroughManager): - def get_queryset(self): - return super(SpotManager, self).get_queryset().filter(secret=False) - - get_query_set = get_queryset - - -class SpotQuerySet(models.query.QuerySet): - def closed(self): - return self.filter(closed=True) - - def secured(self): - return self.filter(secure=True) - - -class Spot(models.Model): - name = models.CharField(max_length=20) - secure = models.BooleanField(default=True) - closed = models.BooleanField(default=False) - secret = models.BooleanField(default=False) - owner = models.ForeignKey(Dude, related_name='spots_owned') - - objects = SpotManager.for_queryset_class(SpotQuerySet)() - - class Tracked(models.Model): name = models.CharField(max_length=20) number = models.IntegerField() diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index bbacfd1..aa25512 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals from datetime import datetime, timedelta -import pickle try: from unittest import skipUnless except ImportError: # Python 2.6 @@ -25,7 +24,7 @@ from model_utils.tests.models import ( InheritanceManagerTestParent, InheritanceManagerTestChild1, InheritanceManagerTestChild2, TimeStamp, Post, Article, Status, StatusPlainTuple, TimeFrame, Monitored, MonitorWhen, MonitorWhenEmpty, StatusManagerAdded, - TimeFrameManagerAdded, Dude, SplitFieldAbstractParent, Car, Spot, + TimeFrameManagerAdded, SplitFieldAbstractParent, ModelTracked, ModelTrackedFK, ModelTrackedNotDefault, ModelTrackedMultiple, InheritedModelTracked, Tracked, TrackedFK, TrackedNotDefault, TrackedNonFieldAttr, TrackedMultiple, InheritedTracked, StatusFieldDefaultFilled, StatusFieldDefaultNotFilled, @@ -1206,115 +1205,6 @@ class SouthFreezingTests(TestCase): self.assertEqual(kwargs['no_check_for_status'], 'True') - -class PassThroughManagerTests(TestCase): - def setUp(self): - Dude.objects.create(name='The Dude', abides=True, has_rug=False) - Dude.objects.create(name='His Dudeness', abides=False, has_rug=True) - Dude.objects.create(name='Duder', abides=False, has_rug=False) - Dude.objects.create(name='El Duderino', abides=True, has_rug=True) - - - def test_chaining(self): - self.assertEqual(Dude.objects.by_name('Duder').count(), 1) - self.assertEqual(Dude.objects.all().by_name('Duder').count(), 1) - self.assertEqual(Dude.abiders.rug_positive().count(), 1) - self.assertEqual(Dude.abiders.all().rug_positive().count(), 1) - - - def test_manager_only_methods(self): - stats = Dude.abiders.get_stats() - self.assertEqual(stats['rug_count'], 1) - with self.assertRaises(AttributeError): - Dude.abiders.all().get_stats() - - - def test_queryset_pickling(self): - qs = Dude.objects.all() - saltyqs = pickle.dumps(qs) - unqs = pickle.loads(saltyqs) - self.assertEqual(unqs.by_name('The Dude').count(), 1) - - - def test_queryset_not_available_on_related_manager(self): - dude = Dude.objects.by_name('Duder').get() - Car.objects.create(name='Ford', owner=dude) - self.assertFalse(hasattr(dude.cars_owned, 'by_name')) - - - def test_using_dir(self): - # make sure introspecing via dir() doesn't actually cause queries, - # just as a sanity check. - with self.assertNumQueries(0): - querysets_to_dir = ( - Dude.objects, - Dude.objects.by_name('Duder'), - Dude.objects.all().by_name('Duder'), - Dude.abiders, - Dude.abiders.rug_positive(), - Dude.abiders.all().rug_positive() - ) - for qs in querysets_to_dir: - self.assertTrue('by_name' in dir(qs)) - self.assertTrue('abiding' in dir(qs)) - self.assertTrue('rug_positive' in dir(qs)) - self.assertTrue('rug_negative' in dir(qs)) - # some standard qs methods - self.assertTrue('count' in dir(qs)) - self.assertTrue('order_by' in dir(qs)) - self.assertTrue('select_related' in dir(qs)) - # make sure it's been de-duplicated - self.assertEqual(1, dir(qs).count('distinct')) - - # manager only method. - self.assertTrue('get_stats' in dir(Dude.abiders)) - # manager only method shouldn't appear on the non AbidingManager - self.assertFalse('get_stats' in dir(Dude.objects)) - # standard manager methods - self.assertTrue('get_query_set' in dir(Dude.abiders)) - self.assertTrue('contribute_to_class' in dir(Dude.abiders)) - - - -class CreatePassThroughManagerTests(TestCase): - def setUp(self): - self.dude = Dude.objects.create(name='El Duderino') - self.other_dude = Dude.objects.create(name='Das Dude') - - def test_reverse_manager(self): - Spot.objects.create( - name='The Crib', owner=self.dude, closed=True, secure=True, - secret=False) - self.assertEqual(self.dude.spots_owned.closed().count(), 1) - Spot.objects.create( - name='The Crux', owner=self.other_dude, closed=True, secure=True, - secret=False - ) - self.assertEqual(self.dude.spots_owned.closed().all().count(), 1) - self.assertEqual(self.dude.spots_owned.closed().count(), 1) - - def test_related_queryset_pickling(self): - Spot.objects.create( - name='The Crib', owner=self.dude, closed=True, secure=True, - secret=False) - qs = self.dude.spots_owned.closed() - pickled_qs = pickle.dumps(qs) - unpickled_qs = pickle.loads(pickled_qs) - self.assertEqual(unpickled_qs.secured().count(), 1) - - def test_related_queryset_superclass_method(self): - Spot.objects.create( - name='The Crib', owner=self.dude, closed=True, secure=True, - secret=False) - Spot.objects.create( - name='The Secret Crib', owner=self.dude, closed=False, secure=True, - secret=True) - self.assertEqual(self.dude.spots_owned.count(), 1) - - def test_related_manager_create(self): - self.dude.spots_owned.create(name='The Crib', closed=True, secure=True) - - class FieldTrackerTestCase(TestCase): tracker = None From bbad2b7b47f1dbec2972e16207d8a94c8175a196 Mon Sep 17 00:00:00 2001 From: jarekwg Date: Thu, 29 Oct 2015 02:00:49 +1100 Subject: [PATCH 056/271] Hide _clone params in kwargs to match django 1.9 signature --- model_utils/managers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/model_utils/managers.py b/model_utils/managers.py index 8e95318..2247d8a 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -53,12 +53,11 @@ class InheritanceQuerySetMixin(object): new_qs.subclasses = subclasses return new_qs - def _clone(self, klass=None, setup=False, **kwargs): + def _clone(self, **kwargs): for name in ['subclasses', '_annotated']: if hasattr(self, name): kwargs[name] = getattr(self, name) - return super(InheritanceQuerySetMixin, self)._clone( - klass, setup, **kwargs) + return super(InheritanceQuerySetMixin, self)._clone(**kwargs) def annotate(self, *args, **kwargs): qset = super(InheritanceQuerySetMixin, self).annotate(*args, **kwargs) From 201aa3bf30568a500ab8ea915d63148b05589c66 Mon Sep 17 00:00:00 2001 From: jarekwg Date: Thu, 29 Oct 2015 08:12:02 +1100 Subject: [PATCH 057/271] Half-assed use of User model raises exceptions in 1.9b1 --- model_utils/tests/tests.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index aa25512..8c86682 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -2,9 +2,9 @@ from __future__ import unicode_literals from datetime import datetime, timedelta try: - from unittest import skipUnless + from unittest import skipIf, skipUnless except ImportError: # Python 2.6 - from django.utils.unittest import skipUnless + from django.utils.unittest import skipIf, skipUnless import django from django.db import models @@ -867,6 +867,8 @@ class InheritanceManagerUsingModelsTests(TestCase): ]) + @skipIf( + django.VERSION == (1, 9, 0, 'beta', 1), "Something shonky in 1.9b1 when using Auth like this") def test_select_subclass_invalid_related_model(self): """ Confirming that giving a stupid model doesn't work. From 01514db83c09379a8a9e327de5380f4efb2f13fc Mon Sep 17 00:00:00 2001 From: jarekwg Date: Thu, 29 Oct 2015 08:12:35 +1100 Subject: [PATCH 058/271] Update MutableField for 1.9 --- model_utils/tests/fields.py | 49 +++++++++++++++++++++++++------------ model_utils/tests/models.py | 4 +-- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/model_utils/tests/fields.py b/model_utils/tests/fields.py index 3f1503a..7c29aa4 100644 --- a/model_utils/tests/fields.py +++ b/model_utils/tests/fields.py @@ -1,26 +1,43 @@ +import django from django.db import models from django.utils.six import with_metaclass, string_types -class MutableField(with_metaclass(models.SubfieldBase, models.TextField)): +def mutable_from_db(value): + if value == '': + return None + try: + if isinstance(value, string_types): + return [int(i) for i in value.split(',')] + except ValueError: + pass + return value - def to_python(self, value): - if value == '': - return None - try: - if isinstance(value, string_types): - return [int(i) for i in value.split(',')] - except ValueError: - pass +def mutable_to_db(value): + if value is None: + return '' + if isinstance(value, list): + value = ','.join((str(i) for i in value)) + return str(value) - return value - def get_db_prep_save(self, value, connection): - if value is None: - return '' +if django.VERSION >= (1, 9, 0): + class MutableField(models.TextField): + def to_python(self, value): + return mutable_from_db(value) - if isinstance(value, list): - value = ','.join((str(i) for i in value)) + def from_db_value(self, value, expression, connection, context): + return mutable_from_db(value) - return super(MutableField, self).get_db_prep_save(value, connection) + def get_db_prep_save(self, value, connection): + value = super(MutableField, self).get_db_prep_save(value, connection) + return mutable_to_db(value) +else: + class MutableField(with_metaclass(models.SubfieldBase, models.TextField)): + def to_python(self, value): + return mutable_from_db(value) + + def get_db_prep_save(self, value, connection): + value = mutable_to_db(value) + return super(MutableField, self).get_db_prep_save(value, connection) diff --git a/model_utils/tests/models.py b/model_utils/tests/models.py index b1903ed..6b82541 100644 --- a/model_utils/tests/models.py +++ b/model_utils/tests/models.py @@ -204,7 +204,7 @@ class FeaturedManager(models.Manager): class Tracked(models.Model): name = models.CharField(max_length=20) number = models.IntegerField() - mutable = MutableField() + mutable = MutableField(default=None) tracker = FieldTracker() @@ -249,7 +249,7 @@ class InheritedTracked(Tracked): class ModelTracked(models.Model): name = models.CharField(max_length=20) number = models.IntegerField() - mutable = MutableField() + mutable = MutableField(default=None) tracker = ModelTracker() From c82ce585b3fa93bfa62dc24311322c4e852340e1 Mon Sep 17 00:00:00 2001 From: jarekwg Date: Thu, 29 Oct 2015 08:26:36 +1100 Subject: [PATCH 059/271] Make tests run against django1.9 --- .travis.yml | 4 ++++ setup.py | 1 + tox.ini | 9 +++++---- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index b24d579..cba3063 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,19 +12,23 @@ env: - TOXENV=py27-django16 - TOXENV=py27-django17 - TOXENV=py27-django18 + - TOXENV=py27-django19 - TOXENV=py27-django_trunk - TOXENV=py32-django15 - TOXENV=py32-django16 - TOXENV=py32-django17 - TOXENV=py32-django18 + - TOXENV=py32-django19 - TOXENV=py32-django_trunk - TOXENV=py33-django15 - TOXENV=py33-django16 - TOXENV=py33-django17 - TOXENV=py33-django18 + - TOXENV=py33-django19 - TOXENV=py33-django_trunk - TOXENV=py34-django17 - TOXENV=py34-django18 + - TOXENV=py34-django19 - TOXENV=py34-django_trunk install: diff --git a/setup.py b/setup.py index a1b49b6..feb5ffc 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,7 @@ setup( 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', 'Framework :: Django', ], zip_safe=False, diff --git a/tox.ini b/tox.ini index 5255cf5..ce8310c 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,8 @@ envlist = py26-django{14,15,16}, py27-django14, py27-django15_nosouth, - py{27,32,33}-django{15,16,17,18,_trunk}, - py34-django{17,18,_trunk}, + py{27,32,33}-django{15,16,17,18,19_trunk}, + py34-django{17,18,19_trunk}, [testenv] basepython = @@ -18,8 +18,9 @@ deps = django14: Django==1.4.18 django15{,_nosouth}: Django==1.5.12 django16: Django==1.6.10 - django17: Django==1.7.3 - django18: Django==1.8a1 + django17: Django==1.7.7 + django18: Django==1.8.5 + django19: Django==1.9b1 django_trunk: https://github.com/django/django/tarball/master django{14,15,16}: South==1.0.2 From 9204fe0ed2cacc7cbe05d6595b920d49cde40b4b Mon Sep 17 00:00:00 2001 From: jarekwg Date: Thu, 29 Oct 2015 08:45:53 +1100 Subject: [PATCH 060/271] Missing commas --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index ce8310c..0c4e8b5 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,8 @@ envlist = py26-django{14,15,16}, py27-django14, py27-django15_nosouth, - py{27,32,33}-django{15,16,17,18,19_trunk}, - py34-django{17,18,19_trunk}, + py{27,32,33}-django{15,16,17,18,19,_trunk}, + py34-django{17,18,19,_trunk}, [testenv] basepython = From c2019fd3706f7c9b689dfe3cf5086e6c9a972be8 Mon Sep 17 00:00:00 2001 From: jarekwg Date: Thu, 29 Oct 2015 09:29:49 +1100 Subject: [PATCH 061/271] Add py35 tests; remove py32&33 tests against django19 --- .travis.yml | 5 +++-- setup.py | 1 + tox.ini | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index cba3063..f4b9c89 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,18 +18,19 @@ env: - TOXENV=py32-django16 - TOXENV=py32-django17 - TOXENV=py32-django18 - - TOXENV=py32-django19 - TOXENV=py32-django_trunk - TOXENV=py33-django15 - TOXENV=py33-django16 - TOXENV=py33-django17 - TOXENV=py33-django18 - - TOXENV=py33-django19 - TOXENV=py33-django_trunk - TOXENV=py34-django17 - TOXENV=py34-django18 - TOXENV=py34-django19 - TOXENV=py34-django_trunk + - TOXENV=py35-django18 + - TOXENV=py35-django19 + - TOXENV=py35-django_trunk install: - pip install --upgrade pip setuptools tox virtualenv coveralls diff --git a/setup.py b/setup.py index feb5ffc..5137408 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ setup( 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Framework :: Django', ], zip_safe=False, diff --git a/tox.ini b/tox.ini index 0c4e8b5..ae7287f 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,9 @@ envlist = py26-django{14,15,16}, py27-django14, py27-django15_nosouth, - py{27,32,33}-django{15,16,17,18,19,_trunk}, + py{27,32,33}-django{15,16,17,18,_trunk}, py34-django{17,18,19,_trunk}, + py35-django{18,19,_trunk}, [testenv] basepython = @@ -12,6 +13,7 @@ basepython = py32: python3.2 py33: python3.3 py34: python3.4 + py35: python3.5 deps = coverage == 3.6 From 1d473ec6a94a0a55f467274d93c5b5603733521d Mon Sep 17 00:00:00 2001 From: jarekwg Date: Thu, 29 Oct 2015 09:33:45 +1100 Subject: [PATCH 062/271] Test InheritanceManager fail against a different model --- model_utils/tests/tests.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index 8c86682..a5f484b 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta try: from unittest import skipIf, skipUnless except ImportError: # Python 2.6 - from django.utils.unittest import skipIf, skipUnless + from django.utils.unittest import skipUnless import django from django.db import models @@ -867,17 +867,14 @@ class InheritanceManagerUsingModelsTests(TestCase): ]) - @skipIf( - django.VERSION == (1, 9, 0, 'beta', 1), "Something shonky in 1.9b1 when using Auth like this") def test_select_subclass_invalid_related_model(self): """ Confirming that giving a stupid model doesn't work. """ - from django.contrib.auth.models import User regex = '^.+? is not a subclass of .+$' with self.assertRaisesRegexp(ValueError, regex): InheritanceManagerTestParent.objects.select_subclasses( - User).order_by('pk') + TimeFrame).order_by('pk') From 81eba92e61cea3162b11d27777ab85fa5a68c82f Mon Sep 17 00:00:00 2001 From: jarekwg Date: Thu, 29 Oct 2015 09:37:21 +1100 Subject: [PATCH 063/271] flakes --- model_utils/models.py | 1 - model_utils/tests/tests.py | 10 ++++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/model_utils/models.py b/model_utils/models.py index 142b77c..3db4073 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import django from django.db import models from django.utils.translation import ugettext_lazy as _ -from django.db.models.fields import FieldDoesNotExist from django.core.exceptions import ImproperlyConfigured if django.VERSION >= (1, 9, 0): from django.db.models.functions import Now diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index a5f484b..dd40506 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from datetime import datetime, timedelta try: - from unittest import skipIf, skipUnless + from unittest import skipUnless except ImportError: # Python 2.6 from django.utils.unittest import skipUnless @@ -1391,15 +1391,13 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): class FieldTrackerMultipleInstancesTests(TestCase): def test_with_deferred_fields_access_multiple(self): - instances = [ - Tracked.objects.create(pk=1, name='foo', number=1), - Tracked.objects.create(pk=2, name='bar', number=2) - ] + Tracked.objects.create(pk=1, name='foo', number=1) + Tracked.objects.create(pk=2, name='bar', number=2) queryset = Tracked.objects.only('id') for instance in queryset: - name = instance.name + instance.name class FieldTrackedModelCustomTests(FieldTrackerTestCase, From dcd288cb09ebc2d973c0a5428418acd8b98e3427 Mon Sep 17 00:00:00 2001 From: jarekwg Date: Thu, 29 Oct 2015 09:42:16 +1100 Subject: [PATCH 064/271] Jumped the gun with py35 tests.. https://github.com/travis-ci/travis-ci/issues/4794 --- .travis.yml | 3 --- setup.py | 1 - tox.ini | 2 -- 3 files changed, 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index f4b9c89..b23f12e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,9 +28,6 @@ env: - TOXENV=py34-django18 - TOXENV=py34-django19 - TOXENV=py34-django_trunk - - TOXENV=py35-django18 - - TOXENV=py35-django19 - - TOXENV=py35-django_trunk install: - pip install --upgrade pip setuptools tox virtualenv coveralls diff --git a/setup.py b/setup.py index 5137408..feb5ffc 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,6 @@ setup( 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', 'Framework :: Django', ], zip_safe=False, diff --git a/tox.ini b/tox.ini index ae7287f..00eea15 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,6 @@ envlist = py27-django14, py27-django15_nosouth, py{27,32,33}-django{15,16,17,18,_trunk}, py34-django{17,18,19,_trunk}, - py35-django{18,19,_trunk}, [testenv] basepython = @@ -13,7 +12,6 @@ basepython = py32: python3.2 py33: python3.3 py34: python3.4 - py35: python3.5 deps = coverage == 3.6 From b06fa8c752d2f810c21354949d46cadfc1f17bdb Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 28 Oct 2015 17:16:50 -0600 Subject: [PATCH 065/271] Update AUTHORS and changelog. --- AUTHORS.rst | 1 + CHANGES.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index b25a3cf..62d6561 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -16,6 +16,7 @@ Gregor Müllegger ivirabyan James Oakley Jannis Leidel +Jarek Glowacki Javier García Sogo Jeff Elmore Keryn Knight diff --git a/CHANGES.rst b/CHANGES.rst index 0c20f13..e191ec4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,8 @@ CHANGES master (unreleased) ------------------- +* Add support for Django 1.9. Drop support for Django 1.6 and earlier. + 2.3.1 (2015-07-20) ------------------ From ce8deed5cac7273a4a04555445eddda958f2fa5b Mon Sep 17 00:00:00 2001 From: Karl WnW Date: Mon, 2 Nov 2015 16:45:41 +0100 Subject: [PATCH 066/271] Fix _clone signature for Django<1.9 InheritanceQuerySetMixin._clone signature conflicts with django ValuesQuerySet._clone code which calls super like this: "c = super(ValuesQuerySet, self)._clone(klass, **kwargs)" --- AUTHORS.rst | 1 + model_utils/managers.py | 5 ++++- model_utils/tests/tests.py | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 62d6561..1776b2b 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -35,4 +35,5 @@ sayane Tony Aldridge Travis Swicegood Trey Hunner +Karl Wan Nan Wo zyegfryed diff --git a/model_utils/managers.py b/model_utils/managers.py index 2247d8a..9392e69 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -53,10 +53,13 @@ class InheritanceQuerySetMixin(object): new_qs.subclasses = subclasses return new_qs - def _clone(self, **kwargs): + def _clone(self, klass=None, setup=False, **kwargs): for name in ['subclasses', '_annotated']: if hasattr(self, name): kwargs[name] = getattr(self, name) + if django.VERSION < (1, 9): + kwargs['klass'] = klass + kwargs['setup'] = setup return super(InheritanceQuerySetMixin, self)._clone(**kwargs) def annotate(self, *args, **kwargs): diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index dd40506..d32bf86 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -763,6 +763,11 @@ class InheritanceManagerTests(TestCase): set(expected_related_names)) + def test_filter_on_values_queryset(self): + queryset = InheritanceManagerTestChild1.objects.values('id').filter(pk=self.child1.pk) + self.assertEqual(list(queryset), [{'id': self.child1.pk}]) + + class InheritanceManagerUsingModelsTests(TestCase): def setUp(self): From 40fae1a6d2ffbf6bb76e1b63868391614fe11208 Mon Sep 17 00:00:00 2001 From: Patryk Zawadzki Date: Wed, 2 Dec 2015 12:15:08 +0100 Subject: [PATCH 067/271] Start testing against Django 1.9 stable --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 00eea15..40aa033 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = py26-django{14,15,16}, py27-django14, py27-django15_nosouth, py{27,32,33}-django{15,16,17,18,_trunk}, + py27-django19, py34-django{17,18,19,_trunk}, [testenv] @@ -20,7 +21,7 @@ deps = django16: Django==1.6.10 django17: Django==1.7.7 django18: Django==1.8.5 - django19: Django==1.9b1 + django19: Django==1.9 django_trunk: https://github.com/django/django/tarball/master django{14,15,16}: South==1.0.2 From 3c8fe6a7d24023bee4237320fb0b461929dd4d63 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 2 Dec 2015 10:43:11 -0700 Subject: [PATCH 068/271] Bump version to 2.4. --- CHANGES.rst | 4 ++-- model_utils/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e191ec4..0c450d7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,8 +1,8 @@ CHANGES ======= -master (unreleased) -------------------- +2.4 (2015-12-03) +---------------- * Add support for Django 1.9. Drop support for Django 1.6 and earlier. diff --git a/model_utils/__init__.py b/model_utils/__init__.py index cb60d23..782fa33 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.4a2' +__version__ = '2.4' From f1f8749fa97c18199132be530bf1c069d360096e Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 2 Dec 2015 10:55:59 -0700 Subject: [PATCH 069/271] Bump version to 2.4.1a1. --- CHANGES.rst | 3 +++ model_utils/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0c450d7..1197557 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,9 @@ CHANGES ======= +master (unreleased) +------------------- + 2.4 (2015-12-03) ---------------- diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 782fa33..b26d1ec 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.4' +__version__ = '2.4.1a1' From 520f1f74d1d1ac8932e1fcc2878b2affb04e0a6b Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 2 Dec 2015 12:47:07 -0700 Subject: [PATCH 070/271] Add missed changelog entry for removal of PassThroughManager. --- CHANGES.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1197557..e2849c7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,8 +7,12 @@ master (unreleased) 2.4 (2015-12-03) ---------------- +* Remove `PassThroughManager`. Use Django's built-in `QuerySet.as_manager()` + and/or `Manager.from_queryset()` utilities instead. + * Add support for Django 1.9. Drop support for Django 1.6 and earlier. + 2.3.1 (2015-07-20) ------------------ From e070237a34f4f3166c796bbbcdbab0280a5f6f80 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 2 Dec 2015 12:54:06 -0700 Subject: [PATCH 071/271] README and changelog updates. --- CHANGES.rst | 2 +- README.rst | 19 ++++++------------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e2849c7..811ae54 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,7 +10,7 @@ master (unreleased) * Remove `PassThroughManager`. Use Django's built-in `QuerySet.as_manager()` and/or `Manager.from_queryset()` utilities instead. -* Add support for Django 1.9. Drop support for Django 1.6 and earlier. +* Add support for Django 1.9. 2.3.1 (2015-07-20) diff --git a/README.rst b/README.rst index b7ddd73..4bddd6c 100644 --- a/README.rst +++ b/README.rst @@ -16,30 +16,23 @@ Django model mixins and utilities. .. _Django: http://www.djangoproject.com/ +This app is available on `PyPI`_. + +.. _PyPI: https://pypi.python.org/pypi/django-model-utils/ + Getting Help ============ Documentation for django-model-utils is available at https://django-model-utils.readthedocs.org/ -This app is available on `PyPI`_. - -.. _PyPI: https://pypi.python.org/pypi/django-model-utils/ - Contributing ============ Please file bugs and send pull requests to the `GitHub repository`_ and `issue -tracker`_. +tracker`_. See `CONTRIBUTING.rst`_ for details. .. _GitHub repository: https://github.com/carljm/django-model-utils/ .. _issue tracker: https://github.com/carljm/django-model-utils/issues - -(Until January 2013 django-model-utils primary development was hosted at -`BitBucket`_; the issue tracker there will remain open until all issues and -pull requests tracked in it are closed, but all new issues should be filed at -GitHub.) - -.. _BitBucket: https://bitbucket.org/carljm/django-model-utils/overview - +.. _CONTRIBUTING.rst: https://github.com/carljm/django-model-utils/blob/master/CONTRIBUTING.rst From acbb22f30086aaf381afac827431af8f317fa4d8 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 2 Dec 2015 12:56:03 -0700 Subject: [PATCH 072/271] Update CONTRIBUTING doc. --- CONTRIBUTING.rst | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 6b2b848..10fbcca 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,26 +1,26 @@ Contributing ============ -Below is a list of tips for submitting issues and pull requests. These are -suggestions and not requirements. +Below is a list of tips for submitting issues and pull requests. Submitting Issues ----------------- -Issues are often easier to reproduce/resolve when they have: +Issues are easier to reproduce/resolve when they have: - A pull request with a failing test demonstrating the issue - A code example that produces the issue consistently - A traceback (when applicable) + Pull Requests ------------- -When creating a pull request, try to: +When creating a pull request: -- Write tests if applicable -- Note important changes in the `CHANGES`_ file -- Update the documentation if needed +- Write tests +- Note user-facing changes in the `CHANGES`_ file +- Update the documentation - Add yourself to the `AUTHORS`_ file - If you have added or changed translated strings, run ``make messages`` to update the ``.po`` translation files, and update translations for any @@ -47,6 +47,7 @@ After you finished editing add yourself to the list of translators. If you have created a new translation, make sure to copy the header from one of the existing translation files. + Testing ------- From d131735e4f74e7c74b79d61bdd0ad7ecc22feac2 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 3 Dec 2015 09:20:42 -0700 Subject: [PATCH 073/271] Fix MANIFEST.in. --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 48f0ac9..04bb992 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,4 +4,4 @@ include LICENSE.txt include MANIFEST.in include README.rst include TODO.rst -locale/*/LC_MESSAGES/django.po \ No newline at end of file +recursive-include locale django.po From 0e5c42377f2c337622d641d10303c6c0fd99f4f6 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 3 Dec 2015 09:30:06 -0700 Subject: [PATCH 074/271] Fix unclosed file handles in setup.py. --- TODO.rst | 4 ---- setup.py | 22 +++++++++++++++------- 2 files changed, 15 insertions(+), 11 deletions(-) delete mode 100644 TODO.rst diff --git a/TODO.rst b/TODO.rst deleted file mode 100644 index 4218c78..0000000 --- a/TODO.rst +++ /dev/null @@ -1,4 +0,0 @@ -TODO -==== - -* Switch to proper test skips once Django 1.3 is minimum supported. diff --git a/setup.py b/setup.py index feb5ffc..8b92d32 100644 --- a/setup.py +++ b/setup.py @@ -1,21 +1,29 @@ -from os.path import join +import os from setuptools import setup, find_packages -long_description = (open('README.rst').read() + - open('CHANGES.rst').read() + - open('TODO.rst').read()) +def long_desc(root_path): + FILES = ['README.rst', 'CHANGES.rst'] + for filename in FILES: + filepath = os.path.realpath(os.path.join(root_path, filename)) + if os.path.isfile(filepath): + with open(filepath, mode='r') as f: + yield f.read() -def get_version(): - with open(join('model_utils', '__init__.py')) as f: +HERE = os.path.abspath(os.path.dirname(__file__)) +long_description = "\n\n".join(long_desc(HERE)) + + +def get_version(root_path): + with open(os.path.join(root_path, 'model_utils', '__init__.py')) as f: for line in f: if line.startswith('__version__ ='): return line.split('=')[1].strip().strip('"\'') setup( name='django-model-utils', - version=get_version(), + version=get_version(HERE), description='Django model mixins and utilities', long_description=long_description, author='Carl Meyer', From 9eeca1083507b40010fc598a878e8903cc79fe94 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 18 Dec 2015 11:32:03 -0700 Subject: [PATCH 075/271] Update PassThroughManager docs to recommend alternatives; fixes GH-201. --- docs/managers.rst | 47 +++-------------------------------------------- 1 file changed, 3 insertions(+), 44 deletions(-) diff --git a/docs/managers.rst b/docs/managers.rst index 0fb3144..2669a1c 100644 --- a/docs/managers.rst +++ b/docs/managers.rst @@ -128,50 +128,9 @@ not required). PassThroughManager ------------------ -A common "gotcha" when defining methods on a custom manager class is that those -same methods are not automatically also available on the QuerySets returned by -that manager, so are not "chainable". This can be counterintuitive, as most of -the public QuerySet API is mirrored on managers. It is possible to create a -custom Manager that returns QuerySets that have the same additional methods, -but this requires boilerplate code. The ``PassThroughManager`` class -(`contributed by Paul McLanahan`_) removes this boilerplate. - -.. _contributed by Paul McLanahan: http://paulm.us/post/3717466639/passthroughmanager-for-django - -To use ``PassThroughManager``, rather than defining a custom manager with -additional methods, define a custom ``QuerySet`` subclass with the additional -methods you want, and pass that ``QuerySet`` subclass to the -``PassThroughManager.for_queryset_class()`` class method. The returned -``PassThroughManager`` subclass will always return instances of your custom -``QuerySet``, and you can also call methods of your custom ``QuerySet`` -directly on the manager: - -.. code-block:: python - - from datetime import datetime - from django.db import models - from django.db.models.query import QuerySet - from model_utils.managers import PassThroughManager - - class PostQuerySet(QuerySet): - def by_author(self, user): - return self.filter(user=user) - - def published(self): - return self.filter(published__lte=datetime.now()) - - def unpublished(self): - return self.filter(published__gte=datetime.now()) - - - class Post(models.Model): - user = models.ForeignKey(User) - published = models.DateTimeField() - - objects = PassThroughManager.for_queryset_class(PostQuerySet)() - - Post.objects.published() - Post.objects.by_author(user=request.user).unpublished() +`PassThroughManager` was removed in django-model-utils 2.4. Use Django's +built-in `QuerySet.as_manager()` and/or `Manager.from_queryset()` utilities +instead. Mixins ------ From d7e37189641093038416f880cd5f9fc9c608eb32 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 8 Feb 2016 15:30:51 -0700 Subject: [PATCH 076/271] Add CI on Python 3.5, drop Python 3.2. --- .travis.yml | 14 ++++++-------- CHANGES.rst | 2 ++ tox.ini | 19 ++++++++++--------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/.travis.yml b/.travis.yml index b23f12e..7b3317b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,18 +7,13 @@ env: - TOXENV=py26-django15 - TOXENV=py26-django16 - TOXENV=py27-django14 - - TOXENV=py27-django15 + - TOXENV=py27-django19 - TOXENV=py27-django15_nosouth + - TOXENV=py27-django15 - TOXENV=py27-django16 - TOXENV=py27-django17 - TOXENV=py27-django18 - - TOXENV=py27-django19 - TOXENV=py27-django_trunk - - TOXENV=py32-django15 - - TOXENV=py32-django16 - - TOXENV=py32-django17 - - TOXENV=py32-django18 - - TOXENV=py32-django_trunk - TOXENV=py33-django15 - TOXENV=py33-django16 - TOXENV=py33-django17 @@ -28,6 +23,9 @@ env: - TOXENV=py34-django18 - TOXENV=py34-django19 - TOXENV=py34-django_trunk + - TOXENV=py35-django18 + - TOXENV=py35-django19 + - TOXENV=py35-django_trunk install: - pip install --upgrade pip setuptools tox virtualenv coveralls @@ -38,8 +36,8 @@ script: matrix: allow_failures: - env: TOXENV=py27-django_trunk - - env: TOXENV=py32-django_trunk - env: TOXENV=py33-django_trunk - env: TOXENV=py34-django_trunk + - env: TOXENV=py35-django_trunk after_success: coveralls diff --git a/CHANGES.rst b/CHANGES.rst index 811ae54..90b649a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,8 @@ CHANGES master (unreleased) ------------------- +* Drop support for Python 3.2. + 2.4 (2015-12-03) ---------------- diff --git a/tox.ini b/tox.ini index 40aa033..84018e9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,10 @@ [tox] envlist = py26-django{14,15,16}, - py27-django14, py27-django15_nosouth, - py{27,32,33}-django{15,16,17,18,_trunk}, - py27-django19, + py27-django{14,19}, py27-django15_nosouth, + py{27,33}-django{15,16,17,18,_trunk}, py34-django{17,18,19,_trunk}, + py35-django{18,19,_trunk}, [testenv] basepython = @@ -13,15 +13,16 @@ basepython = py32: python3.2 py33: python3.3 py34: python3.4 + py35: python3.5 deps = coverage == 3.6 - django14: Django==1.4.18 - django15{,_nosouth}: Django==1.5.12 - django16: Django==1.6.10 - django17: Django==1.7.7 - django18: Django==1.8.5 - django19: Django==1.9 + django14: Django>=1.4,<1.5 + django15{,_nosouth}: Django>=1.5,<1.6 + django16: Django>=1.6,<1.7 + django17: Django>=1.7,<1.8 + django18: Django>=1.8,<1.9 + django19: Django>=1.9,<1.10 django_trunk: https://github.com/django/django/tarball/master django{14,15,16}: South==1.0.2 From 4f39ce9497dc020283a0e7e91cab59d9a2678f6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Bogda=C5=82?= Date: Mon, 8 Feb 2016 22:37:29 +0100 Subject: [PATCH 077/271] Add support for Django 1.10 --- model_utils/managers.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/model_utils/managers.py b/model_utils/managers.py index 9392e69..d8ecf4e 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import django from django.db import models -from django.db.models.fields.related import OneToOneField +from django.db.models.fields.related import OneToOneField, OneToOneRel from django.db.models.query import QuerySet from django.core.exceptions import ObjectDoesNotExist @@ -101,12 +101,20 @@ class InheritanceQuerySetMixin(object): recursively, returning a `list` of strings representing the relations for select_related """ + if django.VERSION < (1, 8): + related_objects = model._meta.get_all_related_objects() + else: + related_objects = [ + f for f in model._meta.get_fields() + if isinstance(f, OneToOneRel)] + rels = [ - rel for rel in model._meta.get_all_related_objects() + rel for rel in related_objects if isinstance(rel.field, OneToOneField) and issubclass(rel.field.model, model) and model is not rel.field.model ] + subclasses = [] if levels: levels -= 1 @@ -135,12 +143,16 @@ class InheritanceQuerySetMixin(object): if levels: levels -= 1 while parent_link is not None: - ancestry.insert(0, parent_link.related.get_accessor_name()) + if django.VERSION < (1, 8): + related = parent_link.related + else: + related = parent_link.rel + ancestry.insert(0, related.get_accessor_name()) if levels or levels is None: if django.VERSION < (1, 8): - parent_model = parent_link.related.parent_model + parent_model = related.parent_model else: - parent_model = parent_link.related.model + parent_model = related.model parent_link = parent_model._meta.get_ancestor_link( self.model) else: From fa72fb152153f6e03a031bb71e990043a5733a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Bogda=C5=82?= Date: Tue, 9 Feb 2016 00:24:10 +0100 Subject: [PATCH 078/271] Update changelog --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 90b649a..b8910a0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,9 @@ master (unreleased) * Drop support for Python 3.2. +* Add support for Django 1.10. + + 2.4 (2015-12-03) ---------------- From 485bba9d73f277d8e12d8e87234aa5eb687018ff Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 8 Feb 2016 16:26:44 -0700 Subject: [PATCH 079/271] Trunk doesn't support Python 3.3, either. --- .travis.yml | 4 +--- tox.ini | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7b3317b..c1f134f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,17 +8,16 @@ env: - TOXENV=py26-django16 - TOXENV=py27-django14 - TOXENV=py27-django19 + - TOXENV=py27-django_trunk - TOXENV=py27-django15_nosouth - TOXENV=py27-django15 - TOXENV=py27-django16 - TOXENV=py27-django17 - TOXENV=py27-django18 - - TOXENV=py27-django_trunk - TOXENV=py33-django15 - TOXENV=py33-django16 - TOXENV=py33-django17 - TOXENV=py33-django18 - - TOXENV=py33-django_trunk - TOXENV=py34-django17 - TOXENV=py34-django18 - TOXENV=py34-django19 @@ -36,7 +35,6 @@ script: matrix: allow_failures: - env: TOXENV=py27-django_trunk - - env: TOXENV=py33-django_trunk - env: TOXENV=py34-django_trunk - env: TOXENV=py35-django_trunk diff --git a/tox.ini b/tox.ini index 84018e9..acbbcb8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,8 @@ [tox] envlist = py26-django{14,15,16}, - py27-django{14,19}, py27-django15_nosouth, - py{27,33}-django{15,16,17,18,_trunk}, + py27-django{14,19,_trunk}, py27-django15_nosouth, + py{27,33}-django{15,16,17,18}, py34-django{17,18,19,_trunk}, py35-django{18,19,_trunk}, From ff3f8e55463359315607dd4a760bdd0cfe54cb58 Mon Sep 17 00:00:00 2001 From: Mike Bryant Date: Thu, 31 Mar 2016 15:26:11 +0100 Subject: [PATCH 080/271] Use all the fields to determine _id variants. Fixes #214 If a tracker is defined on an inherited model, where the parent has a ForeignKey, the tracker will now correctly determine that the field_map takes `fk` -> `fk_id` --- AUTHORS.rst | 1 + CHANGES.rst | 3 +++ model_utils/tests/models.py | 5 +++++ model_utils/tests/tests.py | 7 ++++++- model_utils/tracker.py | 4 ++-- 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 1776b2b..bd9edba 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -22,6 +22,7 @@ Jeff Elmore Keryn Knight Matthew Schinckel Michael van Tellingen +Mike Bryant Mikhail Silonov Patryk Zawadzki Paul McLanahan diff --git a/CHANGES.rst b/CHANGES.rst index b8910a0..31df274 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,9 @@ master (unreleased) * Add support for Django 1.10. +* Track foreign keys on parent models properly when a tracker + is defined on a child model. Fixes GH-214. + 2.4 (2015-12-03) ---------------- diff --git a/model_utils/tests/models.py b/model_utils/tests/models.py index 6b82541..c77c034 100644 --- a/model_utils/tests/models.py +++ b/model_utils/tests/models.py @@ -246,6 +246,11 @@ class InheritedTracked(Tracked): name2 = models.CharField(max_length=20) +class InheritedTrackedFK(TrackedFK): + custom_tracker = FieldTracker(fields=['fk_id']) + custom_tracker_without_id = FieldTracker(fields=['fk']) + + class ModelTracked(models.Model): name = models.CharField(max_length=20) number = models.IntegerField() diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index d32bf86..84e2dd6 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -26,7 +26,7 @@ from model_utils.tests.models import ( StatusPlainTuple, TimeFrame, Monitored, MonitorWhen, MonitorWhenEmpty, StatusManagerAdded, TimeFrameManagerAdded, SplitFieldAbstractParent, ModelTracked, ModelTrackedFK, ModelTrackedNotDefault, ModelTrackedMultiple, InheritedModelTracked, - Tracked, TrackedFK, TrackedNotDefault, TrackedNonFieldAttr, TrackedMultiple, + Tracked, TrackedFK, InheritedTrackedFK, TrackedNotDefault, TrackedNonFieldAttr, TrackedMultiple, InheritedTracked, StatusFieldDefaultFilled, StatusFieldDefaultNotFilled, InheritanceManagerTestChild3, StatusFieldChoicesName) @@ -1682,6 +1682,11 @@ class InheritedFieldTrackerTests(FieldTrackerTests): self.assertRaises(FieldError, self.tracker.has_changed, 'name2') +class FieldTrackerInheritedForeignKeyTests(FieldTrackerForeignKeyTests): + + tracked_class = InheritedTrackedFK + + class ModelTrackerTests(FieldTrackerTests): tracked_class = ModelTracked diff --git a/model_utils/tracker.py b/model_utils/tracker.py index be5a7ff..1c7d386 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -101,7 +101,7 @@ class FieldTracker(object): def get_field_map(self, cls): """Returns dict mapping fields names to model attribute names""" field_map = dict((field, field) for field in self.fields) - all_fields = dict((f.name, f.attname) for f in cls._meta.local_fields) + all_fields = dict((f.name, f.attname) for f in cls._meta.fields) field_map.update(**dict((k, v) for (k, v) in all_fields.items() if k in field_map)) return field_map @@ -113,7 +113,7 @@ class FieldTracker(object): def finalize_class(self, sender, **kwargs): if self.fields is None: - self.fields = (field.attname for field in sender._meta.local_fields) + self.fields = (field.attname for field in sender._meta.fields) self.fields = set(self.fields) self.field_map = self.get_field_map(sender) models.signals.post_init.connect(self.initialize_tracker) From a43027ccee3c2eeb36e1a955013fda80e62b044b Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 18 Apr 2016 15:02:40 -0600 Subject: [PATCH 081/271] Bump version to 2.5. --- CHANGES.rst | 4 ++-- model_utils/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 31df274..c894a0a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,8 +1,8 @@ CHANGES ======= -master (unreleased) -------------------- +2.5 (2016-04-18) +---------------- * Drop support for Python 3.2. diff --git a/model_utils/__init__.py b/model_utils/__init__.py index b26d1ec..5afa1a5 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.4.1a1' +__version__ = '2.5' From c5417eb613cd45bf6f1e8b6c2a867ea49ffd9d27 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 18 Apr 2016 15:08:15 -0600 Subject: [PATCH 082/271] Bump version for dev. --- CHANGES.rst | 4 ++++ model_utils/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index c894a0a..f8fff6a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,10 @@ CHANGES ======= +master (unreleased) +------------------- + + 2.5 (2016-04-18) ---------------- diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 5afa1a5..612fb9f 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.5' +__version__ = '2.5.1a1' From cd9201fb491b279383a1a6d5cd6cd3ba8585330f Mon Sep 17 00:00:00 2001 From: Adam Nelson Date: Fri, 6 May 2016 13:21:12 -0400 Subject: [PATCH 083/271] Dropped Python 3.2 from support list --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 4bddd6c..5642ac1 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ django-model-utils Django model mixins and utilities. ``django-model-utils`` supports `Django`_ 1.4.10 and later on Python 2.6, 2.7, -3.2, 3.3 and 3.4. +3.3 and 3.4. .. _Django: http://www.djangoproject.com/ From d1337d5a7c719a35b7f10079ebb4953632ac8409 Mon Sep 17 00:00:00 2001 From: Artis Avotins Date: Wed, 25 May 2016 12:51:41 +0200 Subject: [PATCH 084/271] Fixed a bug with Django >= 1.9 where `values_list` was called on InheritanceQuerySet with `select_subclasses` applied as strings raised AttributeError exception. Adds a new test case `test_dj19_values_list_on_select_subclasses` --- AUTHORS.rst | 1 + CHANGES.rst | 3 +++ model_utils/managers.py | 6 +++++ model_utils/tests/tests.py | 48 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index bd9edba..668453e 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -2,6 +2,7 @@ ad-m Alejandro Varas Alex Orange Andy Freeland +Artis Avotins Bram Boogaard Carl Meyer Curtis Maloney diff --git a/CHANGES.rst b/CHANGES.rst index f8fff6a..5da09fc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,9 @@ CHANGES master (unreleased) ------------------- +* Fix `InheritanceQuerySet` raising an `AttributeError` exception + under Django 1.9. + 2.5 (2016-04-18) ---------------- diff --git a/model_utils/managers.py b/model_utils/managers.py index d8ecf4e..4f06d2e 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -161,6 +161,12 @@ class InheritanceQuerySetMixin(object): def _get_sub_obj_recurse(self, obj, s): rel, _, s = s.partition(LOOKUP_SEP) + + # Django 1.9: If a primitive type gets passed to this recursive function, + # return None as non-models are not part of inheritance. + if not isinstance(obj, models.Model): + return None + try: node = getattr(obj, rel) except ObjectDoesNotExist: diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index 84e2dd6..14b6329 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -768,6 +768,54 @@ class InheritanceManagerTests(TestCase): self.assertEqual(list(queryset), [{'id': self.child1.pk}]) + @skipUnless(django.VERSION >= (1, 9, 0), "test only applies to Django 1.9+") + def test_dj19_values_list_on_select_subclasses(self): + """ + Using `select_subclasses` in conjunction with `values_list()` raised an + exception in `_get_sub_obj_recurse()` because the result of `values_list()` + is either a `tuple` or primitive objects if `flat=True` is specified, + because no type checking was done prior to fetching child nodes. + + Django versions below 1.9 are not affected by this bug. + """ + + # Querysets are cast to lists to force immediate evaluation. + # No exceptions must be thrown. + + # No argument to select_subclasses + objs_1 = list( + self.get_manager(). + select_subclasses(). + values_list('id') + ) + + # String argument to select_subclasses + objs_2 = list( + self.get_manager(). + select_subclasses( + "inheritancemanagertestchild2" + ). + values_list('id') + ) + + # String argument to select_subclasses + objs_3 = list( + self.get_manager(). + select_subclasses( + InheritanceManagerTestChild2 + ). + values_list('id') + ) + + assert all(( + isinstance(objs_1, list), + isinstance(objs_2, list), + isinstance(objs_3, list), + )) + + assert objs_1 == objs_2 == objs_3 + + class InheritanceManagerUsingModelsTests(TestCase): def setUp(self): From 7ec978e1d8015f6428d72e846b1ee7efaae4adf1 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 27 May 2016 20:32:16 +0000 Subject: [PATCH 085/271] 1.10 fix --- model_utils/tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model_utils/tracker.py b/model_utils/tracker.py index 1c7d386..a9c7f70 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -63,7 +63,7 @@ class FieldInstanceTracker(object): def init_deferred_fields(self): self.instance._deferred_fields = [] - if not self.instance._deferred: + if hasattr(self.instance, '_deferred') and not self.instance._deferred: return class DeferredAttributeTracker(DeferredAttribute): From 699369424cce29cb0bcf30db8e7b82fb1c9fb72a Mon Sep 17 00:00:00 2001 From: Adam Chainz Date: Sat, 4 Jun 2016 11:41:33 +0100 Subject: [PATCH 086/271] Convert readthedocs links for their .org -> .io migration for hosted projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As per [their blog post of the 27th April](https://blog.readthedocs.com/securing-subdomains/) ‘Securing subdomains’: > Starting today, Read the Docs will start hosting projects from subdomains on the domain readthedocs.io, instead of on readthedocs.org. This change addresses some security concerns around site cookies while hosting user generated data on the same domain as our dashboard. Test Plan: Manually visited all the links I’ve modified. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 5642ac1..34769d5 100644 --- a/README.rst +++ b/README.rst @@ -24,7 +24,7 @@ This app is available on `PyPI`_. Getting Help ============ -Documentation for django-model-utils is available at https://django-model-utils.readthedocs.org/ +Documentation for django-model-utils is available at https://django-model-utils.readthedocs.io/ Contributing From 98416539a58790bfb74af9d926ff4ba0d369bd02 Mon Sep 17 00:00:00 2001 From: Daniel Stanton Date: Wed, 13 Jul 2016 17:57:13 +0100 Subject: [PATCH 087/271] Docs update: has_changed supports a single field Closes #225 --- docs/utilities.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/utilities.rst b/docs/utilities.rst index e8d22d2..9a25061 100644 --- a/docs/utilities.rst +++ b/docs/utilities.rst @@ -164,6 +164,17 @@ Returns ``True`` if the given field has changed since the last save: >>> a.tracker.has_changed('body') False +The ``has_changed`` method expects a single field. To check multiple fields: + +.. code-block:: pycon + + >>> a = Post.objects.create(title='First Post', description='First Description') + >>> a.title = 'Welcome' + >>> any(a.tracker.has_changed(field) for field in ('title', 'description')): + True + >>> all(a.tracker.has_changed(field) for field in ('title', 'description')): + False + The ``has_changed`` method relies on ``previous`` to determine whether a field's values has changed. From 8e818d9bfa68d28f3cce44bf0bf0347617dfc8bb Mon Sep 17 00:00:00 2001 From: Daniel Stanton Date: Wed, 13 Jul 2016 17:59:08 +0100 Subject: [PATCH 088/271] Removed colons --- docs/utilities.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/utilities.rst b/docs/utilities.rst index 9a25061..03ac07b 100644 --- a/docs/utilities.rst +++ b/docs/utilities.rst @@ -170,9 +170,9 @@ The ``has_changed`` method expects a single field. To check multiple fields: >>> a = Post.objects.create(title='First Post', description='First Description') >>> a.title = 'Welcome' - >>> any(a.tracker.has_changed(field) for field in ('title', 'description')): + >>> any(a.tracker.has_changed(field) for field in ('title', 'description')) True - >>> all(a.tracker.has_changed(field) for field in ('title', 'description')): + >>> all(a.tracker.has_changed(field) for field in ('title', 'description')) False The ``has_changed`` method relies on ``previous`` to determine whether a From 2827dbdb79c95a5daceb0cfae8661418c5a097de Mon Sep 17 00:00:00 2001 From: Daniel Stanton Date: Wed, 13 Jul 2016 21:08:15 +0100 Subject: [PATCH 089/271] Reduced docs --- docs/utilities.rst | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/docs/utilities.rst b/docs/utilities.rst index 03ac07b..44824f5 100644 --- a/docs/utilities.rst +++ b/docs/utilities.rst @@ -153,7 +153,7 @@ Returns ``None`` when the model instance isn't saved yet. has_changed ~~~~~~~~~~~ -Returns ``True`` if the given field has changed since the last save: +Returns ``True`` if the given field has changed since the last save. The ``has_changed`` method expects a single field: .. code-block:: pycon @@ -164,17 +164,6 @@ Returns ``True`` if the given field has changed since the last save: >>> a.tracker.has_changed('body') False -The ``has_changed`` method expects a single field. To check multiple fields: - -.. code-block:: pycon - - >>> a = Post.objects.create(title='First Post', description='First Description') - >>> a.title = 'Welcome' - >>> any(a.tracker.has_changed(field) for field in ('title', 'description')) - True - >>> all(a.tracker.has_changed(field) for field in ('title', 'description')) - False - The ``has_changed`` method relies on ``previous`` to determine whether a field's values has changed. From 535fe46702fd4d5bdff458637237e9103e5851d1 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 18 Jul 2016 14:47:41 -0600 Subject: [PATCH 090/271] Update Python support info. --- README.rst | 2 +- setup.py | 2 +- tox.ini | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 34769d5..666f35c 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ django-model-utils Django model mixins and utilities. ``django-model-utils`` supports `Django`_ 1.4.10 and later on Python 2.6, 2.7, -3.3 and 3.4. +3.3, 3.4 and 3.5. .. _Django: http://www.djangoproject.com/ diff --git a/setup.py b/setup.py index 8b92d32..d894b58 100644 --- a/setup.py +++ b/setup.py @@ -41,9 +41,9 @@ setup( 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Framework :: Django', ], zip_safe=False, diff --git a/tox.ini b/tox.ini index acbbcb8..105db28 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,6 @@ envlist = basepython = py26: python2.6 py27: python2.7 - py32: python3.2 py33: python3.3 py34: python3.4 py35: python3.5 From 870391dd34e3bc9c0cc7d351236b7ad48029e76a Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 3 Aug 2016 08:17:14 -0600 Subject: [PATCH 091/271] Add requirements.txt. --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fdf05b1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +tox +sphinx From bd78590a31a3126650c4c70f6603ad16e8cb75e3 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 3 Aug 2016 08:17:36 -0600 Subject: [PATCH 092/271] Add Django 1.10 to tox/Travis. --- .travis.yml | 9 ++++++--- tox.ini | 7 ++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index c1f134f..c38cff6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,22 +6,25 @@ env: - TOXENV=py26-django14 - TOXENV=py26-django15 - TOXENV=py26-django16 + - TOXENV=py27-django110 - TOXENV=py27-django14 - - TOXENV=py27-django19 - - TOXENV=py27-django_trunk - - TOXENV=py27-django15_nosouth - TOXENV=py27-django15 + - TOXENV=py27-django15_nosouth - TOXENV=py27-django16 - TOXENV=py27-django17 - TOXENV=py27-django18 + - TOXENV=py27-django19 + - TOXENV=py27-django_trunk - TOXENV=py33-django15 - TOXENV=py33-django16 - TOXENV=py33-django17 - TOXENV=py33-django18 + - TOXENV=py34-django110 - TOXENV=py34-django17 - TOXENV=py34-django18 - TOXENV=py34-django19 - TOXENV=py34-django_trunk + - TOXENV=py35-django110 - TOXENV=py35-django18 - TOXENV=py35-django19 - TOXENV=py35-django_trunk diff --git a/tox.ini b/tox.ini index 105db28..b096f7a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,10 @@ [tox] envlist = py26-django{14,15,16}, - py27-django{14,19,_trunk}, py27-django15_nosouth, + py27-django{14,19,110,_trunk}, py27-django15_nosouth, py{27,33}-django{15,16,17,18}, - py34-django{17,18,19,_trunk}, - py35-django{18,19,_trunk}, + py34-django{17,18,19,110,_trunk}, + py35-django{18,19,110,_trunk}, [testenv] basepython = @@ -22,6 +22,7 @@ deps = django17: Django>=1.7,<1.8 django18: Django>=1.8,<1.9 django19: Django>=1.9,<1.10 + django110: Django>=1.10,<1.11 django_trunk: https://github.com/django/django/tarball/master django{14,15,16}: South==1.0.2 From f2bf61f46c22d99e186126040839a863a4cd15d0 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 3 Aug 2016 08:29:46 -0600 Subject: [PATCH 093/271] Mark Django 1.10 as allowed-failure on Travis, for now, and clarify support in README. --- .travis.yml | 3 +++ README.rst | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c38cff6..6ea0c92 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,6 +37,9 @@ script: matrix: allow_failures: + - env: TOXENV=py27-django110 + - env: TOXENV=py34-django110 + - env: TOXENV=py35-django110 - env: TOXENV=py27-django_trunk - env: TOXENV=py34-django_trunk - env: TOXENV=py35-django_trunk diff --git a/README.rst b/README.rst index 666f35c..001712f 100644 --- a/README.rst +++ b/README.rst @@ -11,8 +11,9 @@ django-model-utils Django model mixins and utilities. -``django-model-utils`` supports `Django`_ 1.4.10 and later on Python 2.6, 2.7, -3.3, 3.4 and 3.5. +``django-model-utils`` supports `Django`_ 1.4 through 1.9 (latest bugfix +release in each series only) on Python 2.6 (through Django 1.6 only), 2.7, 3.3 +(through Django 1.8 only), 3.4 and 3.5. .. _Django: http://www.djangoproject.com/ From 5904008eabd287105ed64121591c157008a35126 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 3 Aug 2016 08:38:59 -0600 Subject: [PATCH 094/271] Bump version and update changelog for 2.5.1 release. --- CHANGES.rst | 13 ++++++++----- model_utils/__init__.py | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5da09fc..a336ded 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,25 +1,28 @@ CHANGES ======= -master (unreleased) +2.5.1 (2016.08.03) ------------------- -* Fix `InheritanceQuerySet` raising an `AttributeError` exception +* Fix `InheritanceQuerySet` raising an `AttributeError` exception under Django 1.9. +* Django 1.10 support regressed with changes between pre-alpha and final + release; 1.10 currently not supported. -2.5 (2016-04-18) + +2.5 (2016.04.18) ---------------- * Drop support for Python 3.2. -* Add support for Django 1.10. +* Add support for Django 1.10 pre-alpha. * Track foreign keys on parent models properly when a tracker is defined on a child model. Fixes GH-214. -2.4 (2015-12-03) +2.4 (2015.12.03) ---------------- * Remove `PassThroughManager`. Use Django's built-in `QuerySet.as_manager()` diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 612fb9f..68ca7d4 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.5.1a1' +__version__ = '2.5.1' From 34e4b6880ff2b5c4571cf54f219b916ee9817d35 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 3 Aug 2016 08:44:29 -0600 Subject: [PATCH 095/271] Bump version to 2.5.2a1. --- CHANGES.rst | 6 +++++- model_utils/__init__.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index a336ded..ac0078e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,9 +1,13 @@ CHANGES ======= -2.5.1 (2016.08.03) +master (unreleased) ------------------- + +2.5.1 (2016.08.03) +------------------ + * Fix `InheritanceQuerySet` raising an `AttributeError` exception under Django 1.9. diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 68ca7d4..5cb8526 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.5.1' +__version__ = '2.5.2a1' From 8619b3d270480283a1cf8947506d1884efedcbc7 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 3 Aug 2016 08:44:42 -0600 Subject: [PATCH 096/271] Add twine to requirements. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index fdf05b1..082f7fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ tox sphinx +twine From da8e8cf3c9e9d16e5d70e477d20ad7b4d9104767 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 9 Aug 2016 15:06:49 -0600 Subject: [PATCH 097/271] Include runtests.py in sdist; bump version to 2.5.2. --- CHANGES.rst | 6 ++++-- MANIFEST.in | 1 + model_utils/__init__.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index ac0078e..fa0bd0e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,8 +1,10 @@ CHANGES ======= -master (unreleased) -------------------- +2.5.2 (2016.08.09) +------------------ + +* Include `runtests.py` in sdist. 2.5.1 (2016.08.03) diff --git a/MANIFEST.in b/MANIFEST.in index 04bb992..351a82c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,3 +5,4 @@ include MANIFEST.in include README.rst include TODO.rst recursive-include locale django.po +include runtests.py diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 5cb8526..e42aa57 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.5.2a1' +__version__ = '2.5.2' From 65b0823e16a8b71a85fd934878ff2c26e0db041e Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 9 Aug 2016 15:09:11 -0600 Subject: [PATCH 098/271] Bump version to 2.5.3a1. --- CHANGES.rst | 4 ++++ model_utils/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index fa0bd0e..f9b0a37 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,10 @@ CHANGES ======= +master (unreleased) +------------------- + + 2.5.2 (2016.08.09) ------------------ diff --git a/model_utils/__init__.py b/model_utils/__init__.py index e42aa57..f896c8c 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.5.2' +__version__ = '2.5.3a1' From d212eb7ca24e70fc5c927332bb246d058966161c Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 10 Aug 2016 07:07:55 -0600 Subject: [PATCH 099/271] Fixes #231 -- clarify that requirements.txt contains dev dependencies. --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 082f7fb..0396424 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ +# Dependencies for development of django-model-utils + tox sphinx twine From 922c49770e189e13240d048d0ee13734f8f77cb1 Mon Sep 17 00:00:00 2001 From: Jarek Glowacki Date: Mon, 15 Aug 2016 12:24:07 +1000 Subject: [PATCH 100/271] Django 1.10 support for FieldTracker --- .travis.yml | 9 --------- model_utils/tracker.py | 46 ++++++++++++++++++++++++++++++------------ 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6ea0c92..ae7bd47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,13 +35,4 @@ install: script: - tox -matrix: - allow_failures: - - env: TOXENV=py27-django110 - - env: TOXENV=py34-django110 - - env: TOXENV=py35-django110 - - env: TOXENV=py27-django_trunk - - env: TOXENV=py34-django_trunk - - env: TOXENV=py35-django_trunk - after_success: coveralls diff --git a/model_utils/tracker.py b/model_utils/tracker.py index a9c7f70..70bfd2a 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -2,8 +2,9 @@ from __future__ import unicode_literals from copy import deepcopy -from django.db import models +import django from django.core.exceptions import FieldError +from django.db import models from django.db.models.query_utils import DeferredAttribute @@ -62,7 +63,7 @@ class FieldInstanceTracker(object): ) def init_deferred_fields(self): - self.instance._deferred_fields = [] + self.instance._deferred_fields = set() if hasattr(self.instance, '_deferred') and not self.instance._deferred: return @@ -76,19 +77,38 @@ class FieldInstanceTracker(object): self.saved_data[field.field_name] = deepcopy(value) return data[field.field_name] - for field in self.fields: - field_obj = self.instance.__class__.__dict__.get(field) - if isinstance(field_obj, DeferredAttribute): - self.instance._deferred_fields.append(field) - - # Django 1.4 - model = None - if hasattr(field_obj, 'model_ref'): - model = field_obj.model_ref() - + if django.VERSION >= (1, 8): + self.instance._deferred_fields = self.instance.get_deferred_fields() + for field in self.instance._deferred_fields: + if django.VERSION >= (1, 10): + # Seems like a dj110 bug; have to consult the __dict__ of each + # parent class to find the desired field. + # TODO: Check if parents are being traversed in the correct order. + combined_dict = {} + for klass in self.instance.__class__._meta.get_parent_list(): + combined_dict.update(klass.__dict__) + combined_dict.update(self.instance.__class__.__dict__) + field_obj = combined_dict.get(field) + else: + field_obj = self.instance.__class__.__dict__.get(field) field_tracker = DeferredAttributeTracker( - field_obj.field_name, model) + field_obj.field_name, None) setattr(self.instance.__class__, field, field_tracker) + else: + for field in self.fields: + field_obj = self.instance.__class__.__dict__.get(field) + if isinstance(field_obj, DeferredAttribute): + self.instance._deferred_fields.add(field) + + # Django 1.4 + if django.VERSION >= (1, 5): + model = None + else: + model = field_obj.model_ref() + + field_tracker = DeferredAttributeTracker( + field_obj.field_name, model) + setattr(self.instance.__class__, field, field_tracker) class FieldTracker(object): From bbc076429aa4c819fe3cfd3bd29001905b25f940 Mon Sep 17 00:00:00 2001 From: Jarek Glowacki Date: Wed, 17 Aug 2016 11:48:27 +1000 Subject: [PATCH 101/271] Nicer solution --- model_utils/tracker.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/model_utils/tracker.py b/model_utils/tracker.py index 70bfd2a..6aa7d8a 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -69,6 +69,8 @@ class FieldInstanceTracker(object): class DeferredAttributeTracker(DeferredAttribute): def __get__(field, instance, owner): + if instance is None: + return field data = instance.__dict__ if data.get(field.field_name, field) is field: instance._deferred_fields.remove(field.field_name) @@ -81,14 +83,7 @@ class FieldInstanceTracker(object): self.instance._deferred_fields = self.instance.get_deferred_fields() for field in self.instance._deferred_fields: if django.VERSION >= (1, 10): - # Seems like a dj110 bug; have to consult the __dict__ of each - # parent class to find the desired field. - # TODO: Check if parents are being traversed in the correct order. - combined_dict = {} - for klass in self.instance.__class__._meta.get_parent_list(): - combined_dict.update(klass.__dict__) - combined_dict.update(self.instance.__class__.__dict__) - field_obj = combined_dict.get(field) + field_obj = getattr(self.instance.__class__, field) else: field_obj = self.instance.__class__.__dict__.get(field) field_tracker = DeferredAttributeTracker( From bc7d8207244fc1ea829e516c812c7948b65c92c6 Mon Sep 17 00:00:00 2001 From: Jarek Glowacki Date: Thu, 18 Aug 2016 12:55:47 +1000 Subject: [PATCH 102/271] Keep trunk tests in allowed_failures --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index ae7bd47..c38cff6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,4 +35,10 @@ install: script: - tox +matrix: + allow_failures: + - env: TOXENV=py27-django_trunk + - env: TOXENV=py34-django_trunk + - env: TOXENV=py35-django_trunk + after_success: coveralls From 9ee6065f811cc27a65e2dfcd5f5b6c9857ba00fb Mon Sep 17 00:00:00 2001 From: Alexey Evseev Date: Mon, 5 Sep 2016 17:51:48 +0300 Subject: [PATCH 103/271] Support Django 1.10 deferred FileField with FieldTracker --- AUTHORS.rst | 1 + CHANGES.rst | 2 + model_utils/tests/models.py | 7 +++ model_utils/tests/tests.py | 106 +++++++++++++++++++++++++++++++++++- model_utils/tracker.py | 50 ++++++++++++----- 5 files changed, 151 insertions(+), 15 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 668453e..1ba4b8f 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,6 +1,7 @@ ad-m Alejandro Varas Alex Orange +Alexey Evseev Andy Freeland Artis Avotins Bram Boogaard diff --git a/CHANGES.rst b/CHANGES.rst index f9b0a37..131a7fb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,8 @@ CHANGES master (unreleased) ------------------- +* Fix issue with field tracker and deferred FileField for Django 1.10. + 2.5.2 (2016.08.09) ------------------ diff --git a/model_utils/tests/models.py b/model_utils/tests/models.py index c77c034..c086838 100644 --- a/model_utils/tests/models.py +++ b/model_utils/tests/models.py @@ -242,6 +242,12 @@ class TrackedMultiple(models.Model): number_tracker = FieldTracker(fields=['number']) +class TrackedFileField(models.Model): + some_file = models.FileField(upload_to='test_location') + + tracker = FieldTracker() + + class InheritedTracked(Tracked): name2 = models.CharField(max_length=20) @@ -281,6 +287,7 @@ class ModelTrackedMultiple(models.Model): name_tracker = ModelTracker(fields=['name']) number_tracker = ModelTracker(fields=['number']) + class InheritedModelTracked(ModelTracked): name2 = models.CharField(max_length=20) diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index 14b6329..719d185 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -27,7 +27,7 @@ from model_utils.tests.models import ( TimeFrameManagerAdded, SplitFieldAbstractParent, ModelTracked, ModelTrackedFK, ModelTrackedNotDefault, ModelTrackedMultiple, InheritedModelTracked, Tracked, TrackedFK, InheritedTrackedFK, TrackedNotDefault, TrackedNonFieldAttr, TrackedMultiple, - InheritedTracked, StatusFieldDefaultFilled, StatusFieldDefaultNotFilled, + InheritedTracked, TrackedFileField, StatusFieldDefaultFilled, StatusFieldDefaultNotFilled, InheritanceManagerTestChild3, StatusFieldChoicesName) @@ -1735,6 +1735,110 @@ class FieldTrackerInheritedForeignKeyTests(FieldTrackerForeignKeyTests): tracked_class = InheritedTrackedFK +class FieldTrackerFileFieldTests(FieldTrackerTestCase): + + tracked_class = TrackedFileField + + def setUp(self): + self.instance = self.tracked_class() + self.tracker = self.instance.tracker + self.some_file = 'something.txt' + self.another_file = 'another.txt' + + def test_pre_save_changed(self): + self.assertChanged(some_file=None) + self.instance.some_file = self.some_file + self.assertChanged(some_file=None) + + def test_pre_save_has_changed(self): + self.assertHasChanged(some_file=True) + self.instance.some_file = self.some_file + self.assertHasChanged(some_file=True) + + def test_pre_save_previous(self): + self.assertPrevious(some_file=None) + self.instance.some_file = self.some_file + self.assertPrevious(some_file=None) + + def test_post_save_changed(self): + self.update_instance(some_file=self.some_file) + self.assertChanged() + previous_file = self.instance.some_file + self.instance.some_file = self.another_file + self.assertChanged(some_file=previous_file) + # test deferred file field + deferred_instance = self.tracked_class.objects.defer('some_file')[0] + deferred_instance.some_file # access field to fetch from database + self.assertChanged(tracker=deferred_instance.tracker) + + previous_file = deferred_instance.some_file + deferred_instance.some_file = self.another_file + self.assertChanged( + tracker=deferred_instance.tracker, + some_file=previous_file, + ) + + def test_post_save_has_changed(self): + self.update_instance(some_file=self.some_file) + self.assertHasChanged(some_file=False) + self.instance.some_file = self.another_file + self.assertHasChanged(some_file=True) + + # test deferred file field + deferred_instance = self.tracked_class.objects.defer('some_file')[0] + deferred_instance.some_file # access field to fetch from database + self.assertHasChanged( + tracker=deferred_instance.tracker, + some_file=False, + ) + + deferred_instance.some_file = self.another_file + self.assertHasChanged( + tracker=deferred_instance.tracker, + some_file=True, + ) + + def test_post_save_previous(self): + self.update_instance(some_file=self.some_file) + previous_file = self.instance.some_file + self.instance.some_file = self.another_file + self.assertPrevious(some_file=previous_file) + + # test deferred file field + deferred_instance = self.tracked_class.objects.defer('some_file')[0] + deferred_instance.some_file # access field to fetch from database + self.assertPrevious( + tracker=deferred_instance.tracker, + some_file=previous_file, + ) + + deferred_instance.some_file = self.another_file + self.assertPrevious( + tracker=deferred_instance.tracker, + some_file=previous_file, + ) + + def test_current(self): + self.assertCurrent(some_file=self.instance.some_file, id=None) + self.instance.some_file = self.some_file + self.assertCurrent(some_file=self.instance.some_file, id=None) + + # test deferred file field + self.instance.save() + deferred_instance = self.tracked_class.objects.defer('some_file')[0] + deferred_instance.some_file # access field to fetch from database + self.assertCurrent( + some_file=self.instance.some_file, + id=self.instance.id, + ) + + self.instance.some_file = self.another_file + self.assertCurrent( + some_file=self.instance.some_file, + id=self.instance.id, + ) + + class ModelTrackerTests(FieldTrackerTests): tracked_class = ModelTracked diff --git a/model_utils/tracker.py b/model_utils/tracker.py index 6aa7d8a..93e4a5d 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -5,9 +5,30 @@ from copy import deepcopy import django from django.core.exceptions import FieldError from django.db import models +from django.db.models.fields.files import FileDescriptor from django.db.models.query_utils import DeferredAttribute +class DescriptorMixin(object): + tracker_instance = None + + def __get__(self, instance, owner): + if instance is None: + return self + was_deferred = False + field_name = self._get_field_name() + if field_name in instance._deferred_fields: + instance._deferred_fields.remove(field_name) + was_deferred = True + value = super(DescriptorMixin, self).__get__(instance, owner) + if was_deferred: + self.tracker_instance.saved_data[field_name] = deepcopy(value) + return value + + def _get_field_name(self): + return self.field_name + + class FieldInstanceTracker(object): def __init__(self, instance, fields, field_map): self.instance = instance @@ -67,17 +88,14 @@ class FieldInstanceTracker(object): if hasattr(self.instance, '_deferred') and not self.instance._deferred: return - class DeferredAttributeTracker(DeferredAttribute): - def __get__(field, instance, owner): - if instance is None: - return field - data = instance.__dict__ - if data.get(field.field_name, field) is field: - instance._deferred_fields.remove(field.field_name) - value = super(DeferredAttributeTracker, field).__get__( - instance, owner) - self.saved_data[field.field_name] = deepcopy(value) - return data[field.field_name] + class DeferredAttributeTracker(DescriptorMixin, DeferredAttribute): + tracker_instance = self + + class FileDescriptorTracker(DescriptorMixin, FileDescriptor): + tracker_instance = self + + def _get_field_name(self): + return self.field.name if django.VERSION >= (1, 8): self.instance._deferred_fields = self.instance.get_deferred_fields() @@ -86,9 +104,13 @@ class FieldInstanceTracker(object): field_obj = getattr(self.instance.__class__, field) else: field_obj = self.instance.__class__.__dict__.get(field) - field_tracker = DeferredAttributeTracker( - field_obj.field_name, None) - setattr(self.instance.__class__, field, field_tracker) + if isinstance(field_obj, FileDescriptor): + field_tracker = FileDescriptorTracker(field_obj.field) + setattr(self.instance.__class__, field, field_tracker) + else: + field_tracker = DeferredAttributeTracker( + field_obj.field_name, None) + setattr(self.instance.__class__, field, field_tracker) else: for field in self.fields: field_obj = self.instance.__class__.__dict__.get(field) From 9e90dde2e8f821bb08a3d3f463036036d65745a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Ganczarek?= Date: Mon, 12 Sep 2016 13:50:03 +0200 Subject: [PATCH 104/271] Add SoftDeletableModel --- AUTHORS.rst | 1 + CHANGES.rst | 3 +++ docs/managers.rst | 8 ++++++++ docs/models.rst | 8 ++++++++ model_utils/managers.py | 34 ++++++++++++++++++++++++++++++++++ model_utils/models.py | 24 +++++++++++++++++++++++- model_utils/tests/models.py | 17 ++++++++++++++++- model_utils/tests/tests.py | 31 ++++++++++++++++++++++++++++++- 8 files changed, 123 insertions(+), 3 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 1ba4b8f..8faaddd 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -40,3 +40,4 @@ Travis Swicegood Trey Hunner Karl Wan Nan Wo zyegfryed +Radosław Jan Ganczarek diff --git a/CHANGES.rst b/CHANGES.rst index 131a7fb..61fe7b8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,9 @@ CHANGES master (unreleased) ------------------- +* Added `SoftDeletableModel` abstract class, its manageer + `SoftDeletableManager` and queryset `SoftDeletableQuerySet`. + * Fix issue with field tracker and deferred FileField for Django 1.10. diff --git a/docs/managers.rst b/docs/managers.rst index 2669a1c..9a626d2 100644 --- a/docs/managers.rst +++ b/docs/managers.rst @@ -132,6 +132,14 @@ PassThroughManager built-in `QuerySet.as_manager()` and/or `Manager.from_queryset()` utilities instead. + +SoftDeletableManager +-------------------- + +Returns only model instances that have the ``is_removed`` field set +to False. Uses ``SoftDeletableQuerySet``, which ensures model instances +won't be removed in bulk, but they will be marked as removed instead. + Mixins ------ diff --git a/docs/models.rst b/docs/models.rst index 7a05c79..51bde8f 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -47,3 +47,11 @@ returns objects with that status only: # this query will only return published articles: Article.published.all() + + +SoftDeletableModel +------------------ + +This abstract base class just provides field ``is_removed`` which is +set to True instead of removing the instance. Entities returned in +default manager are limited to not-deleted instances. diff --git a/model_utils/managers.py b/model_utils/managers.py index 4f06d2e..c6a2989 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -244,3 +244,37 @@ class QueryManagerMixin(object): class QueryManager(QueryManagerMixin, models.Manager): pass + + +class SoftDeletableQuerySet(QuerySet): + """ + QuerySet for SoftDeletableModel. Instead of removing instance sets + its ``is_removed`` field to True. + """ + + def delete(self): + """ + Soft delete objects from queryset (set their ``is_removed`` + field to True) + """ + self.update(is_removed=True) + + +class SoftDeletableManager(models.Manager): + """ + Manager that limits the queryset by default to show only not removed + instances of model. + """ + _queryset_class = SoftDeletableQuerySet + + def get_queryset(self): + """ + Return queryset limited to not removed entries. + """ + kwargs = {'model': self.model, 'using': self._db} + if hasattr(self, '_hints'): + kwargs['hints'] = self._hints + + return SoftDeletableQuerySet(**kwargs).filter(is_removed=False) + + get_query_set = get_queryset diff --git a/model_utils/models.py b/model_utils/models.py index 3db4073..3f7ec8d 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -10,7 +10,7 @@ if django.VERSION >= (1, 9, 0): else: from django.utils.timezone import now -from model_utils.managers import QueryManager +from model_utils.managers import QueryManager, SoftDeletableManager from model_utils.fields import AutoCreatedField, AutoLastModifiedField, \ StatusField, MonitorField @@ -99,3 +99,25 @@ models.signals.class_prepared.connect(add_timeframed_query_manager) def _field_exists(model_class, field_name): return field_name in [f.attname for f in model_class._meta.local_fields] + + +class SoftDeletableModel(models.Model): + """ + An abstract base class model with a ``is_removed`` field that + marks entries that are not going to be used anymore, but are + kept in db for any reason. + Default manager returns only not-removed entries. + """ + is_removed = models.BooleanField(default=False) + + class Meta: + abstract = True + + objects = SoftDeletableManager() + + def delete(self, using=None, keep_parents=False): + """ + Soft delete object (set its ``is_removed`` field to True) + """ + self.is_removed = True + self.save() diff --git a/model_utils/tests/models.py b/model_utils/tests/models.py index c086838..eee735b 100644 --- a/model_utils/tests/models.py +++ b/model_utils/tests/models.py @@ -4,7 +4,12 @@ from django.db import models from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ -from model_utils.models import TimeStampedModel, StatusModel, TimeFramedModel +from model_utils.models import ( + SoftDeletableModel, + StatusModel, + TimeFramedModel, + TimeStampedModel, +) from model_utils.tracker import FieldTracker, ModelTracker from model_utils.managers import QueryManager, InheritanceManager from model_utils.fields import SplitField, MonitorField, StatusField @@ -305,3 +310,13 @@ class StatusFieldDefaultNotFilled(models.Model): class StatusFieldChoicesName(models.Model): NAMED_STATUS = Choices((0, "no", "No"), (1, "yes", "Yes")) status = StatusField(choices_name='NAMED_STATUS') + + +class SoftDeletable(SoftDeletableModel): + """ + Test model with additional manager for full access to model + instances. + """ + name = models.CharField(max_length=20) + + all_objects = models.Manager() diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index 719d185..0c86510 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -28,7 +28,8 @@ from model_utils.tests.models import ( ModelTracked, ModelTrackedFK, ModelTrackedNotDefault, ModelTrackedMultiple, InheritedModelTracked, Tracked, TrackedFK, InheritedTrackedFK, TrackedNotDefault, TrackedNonFieldAttr, TrackedMultiple, InheritedTracked, TrackedFileField, StatusFieldDefaultFilled, StatusFieldDefaultNotFilled, - InheritanceManagerTestChild3, StatusFieldChoicesName) + InheritanceManagerTestChild3, StatusFieldChoicesName, + SoftDeletable) class MigrationsTests(TestCase): @@ -1975,3 +1976,31 @@ class InheritedModelTrackerTests(ModelTrackerTests): self.name2 = 'test' self.assertEqual(self.tracker.previous('name2'), None) self.assertTrue(self.tracker.has_changed('name2')) + + +class SoftDeletableModelTests(TestCase): + + def test_can_only_see_not_removed_entries(self): + SoftDeletable.objects.create(name='a', is_removed=True) + SoftDeletable.objects.create(name='b', is_removed=False) + + queryset = SoftDeletable.objects.all() + + self.assertEqual(queryset.count(), 1) + self.assertEqual(queryset[0].name, 'b') + + def test_instance_cannot_be_fully_deleted(self): + instance = SoftDeletable.objects.create(name='a') + + instance.delete() + + self.assertEqual(SoftDeletable.objects.count(), 0) + self.assertEqual(SoftDeletable.all_objects.count(), 1) + + def test_instance_cannot_be_fully_deleted_via_queryset(self): + SoftDeletable.objects.create(name='a') + + SoftDeletable.objects.all().delete() + + self.assertEqual(SoftDeletable.objects.count(), 0) + self.assertEqual(SoftDeletable.all_objects.count(), 1) From a9a8451fc9822887b3e3f88e4d941b64c9158ec3 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 19 Sep 2016 08:03:42 -0600 Subject: [PATCH 105/271] Bump version to 2.6. --- CHANGES.rst | 4 ++-- model_utils/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 61fe7b8..7c25de5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,8 +1,8 @@ CHANGES ======= -master (unreleased) -------------------- +2.6 (2016.09.19) +---------------- * Added `SoftDeletableModel` abstract class, its manageer `SoftDeletableManager` and queryset `SoftDeletableQuerySet`. diff --git a/model_utils/__init__.py b/model_utils/__init__.py index f896c8c..821a243 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.5.3a1' +__version__ = '2.6' From e1a3cee4d3ff9b54091b64ed8f4cb84a12a80b97 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 19 Sep 2016 08:05:40 -0600 Subject: [PATCH 106/271] Bump version to 2.6.1.a1. --- CHANGES.rst | 4 ++++ model_utils/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7c25de5..a46440e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,10 @@ CHANGES ======= +master (unreleased) +------------------- + + 2.6 (2016.09.19) ---------------- diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 821a243..0df8970 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.6' +__version__ = '2.6.1.a1' From 516c457747d4f3bfff036ec07edc2fddaee65892 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Wed, 9 Nov 2016 19:49:31 +0000 Subject: [PATCH 107/271] SoftDeletableModel: use correct DB connection When deleting a SoftDeletableModel instance, the `using` parameter should be passed down to the `save()` method. https://docs.djangoproject.com/en/1.10/topics/db/multi-db/#selecting-a-d atabase-for-save --- model_utils/models.py | 2 +- model_utils/tests/tests.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/model_utils/models.py b/model_utils/models.py index 3f7ec8d..22e9dc8 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -120,4 +120,4 @@ class SoftDeletableModel(models.Model): Soft delete object (set its ``is_removed`` field to True) """ self.is_removed = True - self.save() + self.save(using=using) diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index 0c86510..17fd67e 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from datetime import datetime, timedelta + try: from unittest import skipUnless except ImportError: # Python 2.6 @@ -9,6 +10,7 @@ except ImportError: # Python 2.6 import django from django.db import models from django.db.models.fields import FieldDoesNotExist +from django.db.utils import ConnectionDoesNotExist from django.utils.six import text_type from django.core.exceptions import ImproperlyConfigured, FieldError from django.core.management import call_command @@ -2004,3 +2006,8 @@ class SoftDeletableModelTests(TestCase): self.assertEqual(SoftDeletable.objects.count(), 0) self.assertEqual(SoftDeletable.all_objects.count(), 1) + + def test_delete_instance_no_connection(self): + obj = SoftDeletable.objects.create(name='a') + + self.assertRaises(ConnectionDoesNotExist, obj.delete, using='other') From ff4afd7288b186901b2bcd1361ec879a7678567b Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Thu, 17 Nov 2016 08:56:59 +0000 Subject: [PATCH 108/271] Add a purge_from_db to SoftDeletableModel --- model_utils/models.py | 12 +++++++++++- model_utils/tests/tests.py | 13 +++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/model_utils/models.py b/model_utils/models.py index 22e9dc8..a7876e7 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals import django +from django.core.exceptions import ImproperlyConfigured from django.db import models from django.utils.translation import ugettext_lazy as _ -from django.core.exceptions import ImproperlyConfigured if django.VERSION >= (1, 9, 0): from django.db.models.functions import Now now = Now() @@ -121,3 +121,13 @@ class SoftDeletableModel(models.Model): """ self.is_removed = True self.save(using=using) + + def purge_from_db(self, using=None, keep_parents=False): + """ + Actually purge the entry from the database + """ + del_kwargs = {'using': using} + # keep_parents option is new in Django 1.9 + if django.VERSION >= (1, 9, 0): + del_kwargs['keep_parents'] = keep_parents + super(SoftDeletableModel, self).delete(**del_kwargs) diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index 17fd67e..39637dd 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -2011,3 +2011,16 @@ class SoftDeletableModelTests(TestCase): obj = SoftDeletable.objects.create(name='a') self.assertRaises(ConnectionDoesNotExist, obj.delete, using='other') + + def test_instance_purge(self): + instance = SoftDeletable.objects.create(name='a') + + instance.purge_from_db() + + self.assertEqual(SoftDeletable.objects.count(), 0) + self.assertEqual(SoftDeletable.all_objects.count(), 0) + + def test_instance_purge_no_connection(self): + instance = SoftDeletable.objects.create(name='a') + + self.assertRaises(ConnectionDoesNotExist, instance.purge_from_db, using='other') From e9d57e60fe23f84c0fc5203d7dd5a0496cb2de15 Mon Sep 17 00:00:00 2001 From: romgar Date: Fri, 18 Nov 2016 00:24:57 +0000 Subject: [PATCH 109/271] Add test to demonstrate issue #241 --- model_utils/tests/models.py | 6 ++++++ model_utils/tests/tests.py | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/model_utils/tests/models.py b/model_utils/tests/models.py index eee735b..7876828 100644 --- a/model_utils/tests/models.py +++ b/model_utils/tests/models.py @@ -105,6 +105,12 @@ class MonitorWhenEmpty(models.Model): name_changed = MonitorField(monitor="name", when=[]) +class DoubleMonitored(models.Model): + name = models.CharField(max_length=25) + name_changed = MonitorField(monitor="name") + name2 = models.CharField(max_length=25) + name_changed2 = MonitorField(monitor="name2") + class Status(StatusModel): STATUS = Choices( diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index 17fd67e..99869cc 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -31,7 +31,7 @@ from model_utils.tests.models import ( Tracked, TrackedFK, InheritedTrackedFK, TrackedNotDefault, TrackedNonFieldAttr, TrackedMultiple, InheritedTracked, TrackedFileField, StatusFieldDefaultFilled, StatusFieldDefaultNotFilled, InheritanceManagerTestChild3, StatusFieldChoicesName, - SoftDeletable) + SoftDeletable, DoubleMonitored) class MigrationsTests(TestCase): @@ -248,6 +248,19 @@ class MonitorWhenEmptyFieldTests(TestCase): self.assertEqual(self.instance.name_changed, self.created) +class MonitorDoubleFieldTests(TestCase): + + def setUp(self): + DoubleMonitored.objects.create(name='Charlie', name2='Charlie2') + + def test_recursion_error_with_only(self): + # Any field passed to only() is generating a recursion error + list(DoubleMonitored.objects.only('id')) + + def test_recursion_error_with_defer(self): + # Only monitored fields passed to defer() are failing + list(DoubleMonitored.objects.defer('name')) + class StatusFieldTests(TestCase): From 72158f182004df5113af18b8494ff9fbcacab937 Mon Sep 17 00:00:00 2001 From: romgar Date: Fri, 18 Nov 2016 23:11:23 +0000 Subject: [PATCH 110/271] Avoid to directly initialise a monitored field if defered to avoid recursion issue --- model_utils/fields.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/model_utils/fields.py b/model_utils/fields.py index 805c707..0a3f9cf 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -110,8 +110,11 @@ class MonitorField(models.DateTimeField): return getattr(instance, self.monitor) def _save_initial(self, sender, instance, **kwargs): + if self.monitor in instance.get_deferred_fields(): + # Fix related to issue #241 to avoid recursive error on double monitor fields + return setattr(instance, self.monitor_attname, - self.get_monitored_value(instance)) + self.get_monitored_value(instance)) def pre_save(self, model_instance, add): value = now() From 2455c983fc913f58eba7f91f7550ded326a2ecee Mon Sep 17 00:00:00 2001 From: romgar Date: Fri, 18 Nov 2016 23:12:39 +0000 Subject: [PATCH 111/271] Restore initial indentation --- model_utils/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model_utils/fields.py b/model_utils/fields.py index 0a3f9cf..f54f0ee 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -114,7 +114,7 @@ class MonitorField(models.DateTimeField): # Fix related to issue #241 to avoid recursive error on double monitor fields return setattr(instance, self.monitor_attname, - self.get_monitored_value(instance)) + self.get_monitored_value(instance)) def pre_save(self, model_instance, add): value = now() From 93dd940a5ddefd6a8d811e4fde98c6d12f394026 Mon Sep 17 00:00:00 2001 From: romgar Date: Fri, 18 Nov 2016 23:31:45 +0000 Subject: [PATCH 112/271] Remove defered fields in _save_initial only for Django 1.10+ --- model_utils/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model_utils/fields.py b/model_utils/fields.py index f54f0ee..fe9d4a8 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -110,7 +110,7 @@ class MonitorField(models.DateTimeField): return getattr(instance, self.monitor) def _save_initial(self, sender, instance, **kwargs): - if self.monitor in instance.get_deferred_fields(): + if django.VERSION >= (1, 10) and self.monitor in instance.get_deferred_fields(): # Fix related to issue #241 to avoid recursive error on double monitor fields return setattr(instance, self.monitor_attname, From e2440a6872dc195d2e7446bf9f68832477bbd68f Mon Sep 17 00:00:00 2001 From: romgar Date: Sat, 19 Nov 2016 12:08:48 +0000 Subject: [PATCH 113/271] Add tests to prevent regression in MonitorField behaviour if we filter out deferred fields in _save_initial --- model_utils/tests/tests.py | 9 +++++++++ tox.ini | 1 + 2 files changed, 10 insertions(+) diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index 99869cc..3d3f2f7 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -2,6 +2,8 @@ from __future__ import unicode_literals from datetime import datetime, timedelta +from freezegun import freeze_time + try: from unittest import skipUnless except ImportError: # Python 2.6 @@ -261,6 +263,13 @@ class MonitorDoubleFieldTests(TestCase): # Only monitored fields passed to defer() are failing list(DoubleMonitored.objects.defer('name')) + def test_monitor_still_works_with_deferred_fields_filtered_out_of_save_initial(self): + obj = DoubleMonitored.objects.defer('name').get(name='Charlie') + with freeze_time("2016-12-01"): + obj.name = 'Charlie2' + obj.save() + self.assertEqual(obj.name_changed, datetime(2016, 12, 1)) + class StatusFieldTests(TestCase): diff --git a/tox.ini b/tox.ini index b096f7a..6ce334e 100644 --- a/tox.ini +++ b/tox.ini @@ -25,5 +25,6 @@ deps = django110: Django>=1.10,<1.11 django_trunk: https://github.com/django/django/tarball/master django{14,15,16}: South==1.0.2 + freezegun == 0.3.8 commands = coverage run -a setup.py test From 8ecf75144bd2b7609b5293d5aee557b163702ae3 Mon Sep 17 00:00:00 2001 From: romgar Date: Sun, 20 Nov 2016 22:08:07 +0000 Subject: [PATCH 114/271] Update tests to use freezegun to avoid time resolution issues on Windows --- model_utils/tests/tests.py | 56 +++++++++++++++++++++++--------------- tox.ini | 1 + 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index 17fd67e..0c76506 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -7,6 +7,8 @@ try: except ImportError: # Python 2.6 from django.utils.unittest import skipUnless +from freezegun import freeze_time + import django from django.db import models from django.db.models.fields import FieldDoesNotExist @@ -152,8 +154,9 @@ class SplitFieldTests(TestCase): class MonitorFieldTests(TestCase): def setUp(self): - self.instance = Monitored(name='Charlie') - self.created = self.instance.name_changed + with freeze_time(datetime(2016, 1, 1, 10, 0, 0)): + self.instance = Monitored(name='Charlie') + self.created = self.instance.name_changed def test_save_no_change(self): @@ -162,9 +165,10 @@ class MonitorFieldTests(TestCase): def test_save_changed(self): - self.instance.name = 'Maria' - self.instance.save() - self.assertTrue(self.instance.name_changed > self.created) + with freeze_time(datetime(2016, 1, 1, 12, 0, 0)): + self.instance.name = 'Maria' + self.instance.save() + self.assertEqual(self.instance.name_changed, datetime(2016, 1, 1, 12, 0, 0)) def test_double_save(self): @@ -186,8 +190,9 @@ class MonitorWhenFieldTests(TestCase): Will record changes only when name is 'Jose' or 'Maria' """ def setUp(self): - self.instance = MonitorWhen(name='Charlie') - self.created = self.instance.name_changed + with freeze_time(datetime(2016, 1, 1, 10, 0, 0)): + self.instance = MonitorWhen(name='Charlie') + self.created = self.instance.name_changed def test_save_no_change(self): @@ -196,15 +201,17 @@ class MonitorWhenFieldTests(TestCase): def test_save_changed_to_Jose(self): - self.instance.name = 'Jose' - self.instance.save() - self.assertTrue(self.instance.name_changed > self.created) + with freeze_time(datetime(2016, 1, 1, 12, 0, 0)): + self.instance.name = 'Jose' + self.instance.save() + self.assertEqual(self.instance.name_changed, datetime(2016, 1, 1, 12, 0, 0)) def test_save_changed_to_Maria(self): - self.instance.name = 'Maria' - self.instance.save() - self.assertTrue(self.instance.name_changed > self.created) + with freeze_time(datetime(2016, 1, 1, 12, 0, 0)): + self.instance.name = 'Maria' + self.instance.save() + self.assertEqual(self.instance.name_changed, datetime(2016, 1, 1, 12, 0, 0)) def test_save_changed_to_Pedro(self): @@ -1094,16 +1101,19 @@ class InheritanceManagerRelatedTests(InheritanceManagerTests): class TimeStampedModelTests(TestCase): def test_created(self): - t1 = TimeStamp.objects.create() - t2 = TimeStamp.objects.create() - self.assertTrue(t2.created > t1.created) + with freeze_time(datetime(2016, 1, 1)): + t1 = TimeStamp.objects.create() + self.assertEqual(t1.created, datetime(2016, 1, 1)) def test_modified(self): - t1 = TimeStamp.objects.create() - t2 = TimeStamp.objects.create() - t1.save() - self.assertTrue(t2.modified < t1.modified) + with freeze_time(datetime(2016, 1, 1)): + t1 = TimeStamp.objects.create() + + with freeze_time(datetime(2016, 1, 2)): + t1.save() + + self.assertEqual(t1.modified, datetime(2016, 1, 2)) @@ -1159,9 +1169,11 @@ class StatusModelTests(TestCase): def test_created(self): - c1 = self.model.objects.create() + with freeze_time(datetime(2016, 1, 1)): + c1 = self.model.objects.create() + self.assertTrue(c1.status_changed, datetime(2016, 1, 1)) + c2 = self.model.objects.create() - self.assertTrue(c2.status_changed > c1.status_changed) self.assertEqual(self.model.active.count(), 2) self.assertEqual(self.model.deleted.count(), 0) diff --git a/tox.ini b/tox.ini index b096f7a..6ce334e 100644 --- a/tox.ini +++ b/tox.ini @@ -25,5 +25,6 @@ deps = django110: Django>=1.10,<1.11 django_trunk: https://github.com/django/django/tarball/master django{14,15,16}: South==1.0.2 + freezegun == 0.3.8 commands = coverage run -a setup.py test From 68bc61e82517cb01fa36c0d007091a8670425913 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 21 Nov 2016 11:36:25 -0800 Subject: [PATCH 115/271] Update changelog and AUTHORS. --- AUTHORS.rst | 1 + CHANGES.rst | 3 +++ 2 files changed, 4 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 8faaddd..4dd3044 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -31,6 +31,7 @@ Paul McLanahan Philipp Steinhardt Rinat Shigapov Rodney Folz +Romain Garrigues rsenkbeil Ryan Kaskel Simon Meers diff --git a/CHANGES.rst b/CHANGES.rst index a46440e..421c6bf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,9 @@ CHANGES master (unreleased) ------------------- +* Fix infinite recursion with multiple `MonitorField` and `defer()` or `only()` +on Django 1.10+. Thanks Romain Garrigues. Merge of #242, fixes #241. + 2.6 (2016.09.19) ---------------- From 6fefc53c58ffb429ad5d33ce89cc3a505a7df5b9 Mon Sep 17 00:00:00 2001 From: romgar Date: Wed, 23 Nov 2016 23:49:53 +0000 Subject: [PATCH 116/271] Separate tests in small files, making it easier to edit/add new ones/not conflict on different merge requests --- model_utils/tests/helpers.py | 5 + model_utils/tests/test_choices.py | 261 +++ model_utils/tests/test_fields/__init__.py | 5 + .../tests/test_fields/test_field_tracker.py | 593 +++++ .../tests/test_fields/test_monitor_field.py | 120 + .../tests/test_fields/test_split_field.py | 78 + .../tests/test_fields/test_status_field.py | 32 + model_utils/tests/test_managers/__init__.py | 4 + .../test_managers/test_inheritance_manager.py | 500 ++++ .../tests/test_managers/test_query_manager.py | 29 + .../test_managers/test_status_manager.py | 23 + model_utils/tests/test_miscellaneous.py | 60 + model_utils/tests/test_models/__init__.py | 6 + .../tests/test_models/test_model_tracker.py | 150 ++ .../test_models/test_softdeletable_model.py | 38 + .../tests/test_models/test_status_model.py | 46 + .../test_models/test_timeframed_model.py | 47 + .../test_models/test_timestamped_model.py | 25 + model_utils/tests/tests.py | 2053 +---------------- 19 files changed, 2028 insertions(+), 2047 deletions(-) create mode 100644 model_utils/tests/helpers.py create mode 100644 model_utils/tests/test_choices.py create mode 100644 model_utils/tests/test_fields/__init__.py create mode 100644 model_utils/tests/test_fields/test_field_tracker.py create mode 100644 model_utils/tests/test_fields/test_monitor_field.py create mode 100644 model_utils/tests/test_fields/test_split_field.py create mode 100644 model_utils/tests/test_fields/test_status_field.py create mode 100644 model_utils/tests/test_managers/__init__.py create mode 100644 model_utils/tests/test_managers/test_inheritance_manager.py create mode 100644 model_utils/tests/test_managers/test_query_manager.py create mode 100644 model_utils/tests/test_managers/test_status_manager.py create mode 100644 model_utils/tests/test_miscellaneous.py create mode 100644 model_utils/tests/test_models/__init__.py create mode 100644 model_utils/tests/test_models/test_model_tracker.py create mode 100644 model_utils/tests/test_models/test_softdeletable_model.py create mode 100644 model_utils/tests/test_models/test_status_model.py create mode 100644 model_utils/tests/test_models/test_timeframed_model.py create mode 100644 model_utils/tests/test_models/test_timestamped_model.py diff --git a/model_utils/tests/helpers.py b/model_utils/tests/helpers.py new file mode 100644 index 0000000..499dd52 --- /dev/null +++ b/model_utils/tests/helpers.py @@ -0,0 +1,5 @@ + +try: + from unittest import skipUnless +except ImportError: # Python 2.6 + from django.utils.unittest import skipUnless diff --git a/model_utils/tests/test_choices.py b/model_utils/tests/test_choices.py new file mode 100644 index 0000000..a503405 --- /dev/null +++ b/model_utils/tests/test_choices.py @@ -0,0 +1,261 @@ +from __future__ import unicode_literals + +from django.test import TestCase + +from model_utils import Choices + + +class ChoicesTests(TestCase): + def setUp(self): + self.STATUS = Choices('DRAFT', 'PUBLISHED') + + def test_getattr(self): + self.assertEqual(self.STATUS.DRAFT, 'DRAFT') + + def test_indexing(self): + self.assertEqual(self.STATUS['PUBLISHED'], 'PUBLISHED') + + def test_iteration(self): + self.assertEqual(tuple(self.STATUS), (('DRAFT', 'DRAFT'), ('PUBLISHED', 'PUBLISHED'))) + + def test_len(self): + self.assertEqual(len(self.STATUS), 2) + + def test_repr(self): + self.assertEqual(repr(self.STATUS), "Choices" + repr(( + ('DRAFT', 'DRAFT', 'DRAFT'), + ('PUBLISHED', 'PUBLISHED', 'PUBLISHED'), + ))) + + def test_wrong_length_tuple(self): + with self.assertRaises(ValueError): + Choices(('a',)) + + def test_contains_value(self): + self.assertTrue('PUBLISHED' in self.STATUS) + self.assertTrue('DRAFT' in self.STATUS) + + def test_doesnt_contain_value(self): + self.assertFalse('UNPUBLISHED' in self.STATUS) + + def test_deepcopy(self): + import copy + self.assertEqual(list(self.STATUS), + list(copy.deepcopy(self.STATUS))) + + def test_equality(self): + self.assertEqual(self.STATUS, Choices('DRAFT', 'PUBLISHED')) + + def test_inequality(self): + self.assertNotEqual(self.STATUS, ['DRAFT', 'PUBLISHED']) + self.assertNotEqual(self.STATUS, Choices('DRAFT')) + + def test_composability(self): + self.assertEqual(Choices('DRAFT') + Choices('PUBLISHED'), self.STATUS) + self.assertEqual(Choices('DRAFT') + ('PUBLISHED',), self.STATUS) + self.assertEqual(('DRAFT',) + Choices('PUBLISHED'), self.STATUS) + + def test_option_groups(self): + c = Choices(('group a', ['one', 'two']), ['group b', ('three',)]) + self.assertEqual( + list(c), + [ + ('group a', [('one', 'one'), ('two', 'two')]), + ('group b', [('three', 'three')]), + ], + ) + + +class LabelChoicesTests(ChoicesTests): + def setUp(self): + self.STATUS = Choices( + ('DRAFT', 'is draft'), + ('PUBLISHED', 'is published'), + 'DELETED', + ) + + def test_iteration(self): + self.assertEqual(tuple(self.STATUS), ( + ('DRAFT', 'is draft'), + ('PUBLISHED', 'is published'), + ('DELETED', 'DELETED')) + ) + + def test_indexing(self): + self.assertEqual(self.STATUS['PUBLISHED'], 'is published') + + def test_default(self): + self.assertEqual(self.STATUS.DELETED, 'DELETED') + + def test_provided(self): + self.assertEqual(self.STATUS.DRAFT, 'DRAFT') + + def test_len(self): + self.assertEqual(len(self.STATUS), 3) + + def test_equality(self): + self.assertEqual(self.STATUS, Choices( + ('DRAFT', 'is draft'), + ('PUBLISHED', 'is published'), + 'DELETED', + )) + + def test_inequality(self): + self.assertNotEqual(self.STATUS, [ + ('DRAFT', 'is draft'), + ('PUBLISHED', 'is published'), + 'DELETED' + ]) + self.assertNotEqual(self.STATUS, Choices('DRAFT')) + + def test_repr(self): + self.assertEqual(repr(self.STATUS), "Choices" + repr(( + ('DRAFT', 'DRAFT', 'is draft'), + ('PUBLISHED', 'PUBLISHED', 'is published'), + ('DELETED', 'DELETED', 'DELETED'), + ))) + + def test_contains_value(self): + self.assertTrue('PUBLISHED' in self.STATUS) + self.assertTrue('DRAFT' in self.STATUS) + # This should be True, because both the display value + # and the internal representation are both DELETED. + self.assertTrue('DELETED' in self.STATUS) + + def test_doesnt_contain_value(self): + self.assertFalse('UNPUBLISHED' in self.STATUS) + + def test_doesnt_contain_display_value(self): + self.assertFalse('is draft' in self.STATUS) + + def test_composability(self): + self.assertEqual( + Choices(('DRAFT', 'is draft',)) + Choices(('PUBLISHED', 'is published'), 'DELETED'), + self.STATUS + ) + + self.assertEqual( + (('DRAFT', 'is draft',),) + Choices(('PUBLISHED', 'is published'), 'DELETED'), + self.STATUS + ) + + self.assertEqual( + Choices(('DRAFT', 'is draft',)) + (('PUBLISHED', 'is published'), 'DELETED'), + self.STATUS + ) + + def test_option_groups(self): + c = Choices( + ('group a', [(1, 'one'), (2, 'two')]), + ['group b', ((3, 'three'),)] + ) + self.assertEqual( + list(c), + [ + ('group a', [(1, 'one'), (2, 'two')]), + ('group b', [(3, 'three')]), + ], + ) + + +class IdentifierChoicesTests(ChoicesTests): + def setUp(self): + self.STATUS = Choices( + (0, 'DRAFT', 'is draft'), + (1, 'PUBLISHED', 'is published'), + (2, 'DELETED', 'is deleted')) + + def test_iteration(self): + self.assertEqual(tuple(self.STATUS), ( + (0, 'is draft'), + (1, 'is published'), + (2, 'is deleted'))) + + def test_indexing(self): + self.assertEqual(self.STATUS[1], 'is published') + + def test_getattr(self): + self.assertEqual(self.STATUS.DRAFT, 0) + + def test_len(self): + self.assertEqual(len(self.STATUS), 3) + + def test_repr(self): + self.assertEqual(repr(self.STATUS), "Choices" + repr(( + (0, 'DRAFT', 'is draft'), + (1, 'PUBLISHED', 'is published'), + (2, 'DELETED', 'is deleted'), + ))) + + def test_contains_value(self): + self.assertTrue(0 in self.STATUS) + self.assertTrue(1 in self.STATUS) + self.assertTrue(2 in self.STATUS) + + def test_doesnt_contain_value(self): + self.assertFalse(3 in self.STATUS) + + def test_doesnt_contain_display_value(self): + self.assertFalse('is draft' in self.STATUS) + + def test_doesnt_contain_python_attr(self): + self.assertFalse('PUBLISHED' in self.STATUS) + + def test_equality(self): + self.assertEqual(self.STATUS, Choices( + (0, 'DRAFT', 'is draft'), + (1, 'PUBLISHED', 'is published'), + (2, 'DELETED', 'is deleted') + )) + + def test_inequality(self): + self.assertNotEqual(self.STATUS, [ + (0, 'DRAFT', 'is draft'), + (1, 'PUBLISHED', 'is published'), + (2, 'DELETED', 'is deleted') + ]) + self.assertNotEqual(self.STATUS, Choices('DRAFT')) + + def test_composability(self): + self.assertEqual( + Choices( + (0, 'DRAFT', 'is draft'), + (1, 'PUBLISHED', 'is published') + ) + Choices( + (2, 'DELETED', 'is deleted'), + ), + self.STATUS + ) + + self.assertEqual( + Choices( + (0, 'DRAFT', 'is draft'), + (1, 'PUBLISHED', 'is published') + ) + ( + (2, 'DELETED', 'is deleted'), + ), + self.STATUS + ) + + self.assertEqual( + ( + (0, 'DRAFT', 'is draft'), + (1, 'PUBLISHED', 'is published') + ) + Choices( + (2, 'DELETED', 'is deleted'), + ), + self.STATUS + ) + + def test_option_groups(self): + c = Choices( + ('group a', [(1, 'ONE', 'one'), (2, 'TWO', 'two')]), + ['group b', ((3, 'THREE', 'three'),)] + ) + self.assertEqual( + list(c), + [ + ('group a', [(1, 'one'), (2, 'two')]), + ('group b', [(3, 'three')]), + ], + ) diff --git a/model_utils/tests/test_fields/__init__.py b/model_utils/tests/test_fields/__init__.py new file mode 100644 index 0000000..23a349f --- /dev/null +++ b/model_utils/tests/test_fields/__init__.py @@ -0,0 +1,5 @@ +# Needed for Django 1.4/1.5 test runner +from .test_field_tracker import * +from .test_monitor_field import * +from .test_split_field import * +from .test_status_field import * diff --git a/model_utils/tests/test_fields/test_field_tracker.py b/model_utils/tests/test_fields/test_field_tracker.py new file mode 100644 index 0000000..4bd96e1 --- /dev/null +++ b/model_utils/tests/test_fields/test_field_tracker.py @@ -0,0 +1,593 @@ +from __future__ import unicode_literals + +import django +from django.core.exceptions import FieldError +from django.test import TestCase + +from model_utils import FieldTracker +from model_utils.tests.helpers import skipUnless +from model_utils.tests.models import ( + Tracked, TrackedFK, InheritedTrackedFK, TrackedNotDefault, TrackedNonFieldAttr, TrackedMultiple, + InheritedTracked, TrackedFileField) + + +class FieldTrackerTestCase(TestCase): + + tracker = None + + def assertHasChanged(self, **kwargs): + tracker = kwargs.pop('tracker', self.tracker) + for field, value in kwargs.items(): + if value is None: + with self.assertRaises(FieldError): + tracker.has_changed(field) + else: + self.assertEqual(tracker.has_changed(field), value) + + def assertPrevious(self, **kwargs): + tracker = kwargs.pop('tracker', self.tracker) + for field, value in kwargs.items(): + self.assertEqual(tracker.previous(field), value) + + def assertChanged(self, **kwargs): + tracker = kwargs.pop('tracker', self.tracker) + self.assertEqual(tracker.changed(), kwargs) + + def assertCurrent(self, **kwargs): + tracker = kwargs.pop('tracker', self.tracker) + self.assertEqual(tracker.current(), kwargs) + + def update_instance(self, **kwargs): + for field, value in kwargs.items(): + setattr(self.instance, field, value) + self.instance.save() + + +class FieldTrackerCommonTests(object): + + def test_pre_save_previous(self): + self.assertPrevious(name=None, number=None) + self.instance.name = 'new age' + self.instance.number = 8 + self.assertPrevious(name=None, number=None) + + +class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): + + tracked_class = Tracked + + def setUp(self): + self.instance = self.tracked_class() + self.tracker = self.instance.tracker + + def test_descriptor(self): + self.assertTrue(isinstance(self.tracked_class.tracker, FieldTracker)) + + def test_pre_save_changed(self): + self.assertChanged(name=None) + self.instance.name = 'new age' + self.assertChanged(name=None) + self.instance.number = 8 + self.assertChanged(name=None, number=None) + self.instance.name = '' + self.assertChanged(name=None, number=None) + self.instance.mutable = [1,2,3] + self.assertChanged(name=None, number=None, mutable=None) + + def test_pre_save_has_changed(self): + self.assertHasChanged(name=True, number=False, mutable=False) + self.instance.name = 'new age' + self.assertHasChanged(name=True, number=False, mutable=False) + self.instance.number = 7 + self.assertHasChanged(name=True, number=True) + self.instance.mutable = [1,2,3] + self.assertHasChanged(name=True, number=True, mutable=True) + + def test_first_save(self): + self.assertHasChanged(name=True, number=False, mutable=False) + self.assertPrevious(name=None, number=None, mutable=None) + self.assertCurrent(name='', number=None, id=None, mutable=None) + self.assertChanged(name=None) + self.instance.name = 'retro' + self.instance.number = 4 + self.instance.mutable = [1,2,3] + self.assertHasChanged(name=True, number=True, mutable=True) + self.assertPrevious(name=None, number=None, mutable=None) + self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) + self.assertChanged(name=None, number=None, mutable=None) + # Django 1.4 doesn't have update_fields + if django.VERSION >= (1, 5, 0): + self.instance.save(update_fields=[]) + self.assertHasChanged(name=True, number=True, mutable=True) + self.assertPrevious(name=None, number=None, mutable=None) + self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) + self.assertChanged(name=None, number=None, mutable=None) + with self.assertRaises(ValueError): + self.instance.save(update_fields=['number']) + + def test_post_save_has_changed(self): + self.update_instance(name='retro', number=4, mutable=[1,2,3]) + self.assertHasChanged(name=False, number=False, mutable=False) + self.instance.name = 'new age' + self.assertHasChanged(name=True, number=False) + self.instance.number = 8 + self.assertHasChanged(name=True, number=True) + self.instance.mutable[1] = 4 + self.assertHasChanged(name=True, number=True, mutable=True) + self.instance.name = 'retro' + self.assertHasChanged(name=False, number=True, mutable=True) + + def test_post_save_previous(self): + self.update_instance(name='retro', number=4, mutable=[1,2,3]) + self.instance.name = 'new age' + self.assertPrevious(name='retro', number=4, mutable=[1,2,3]) + self.instance.mutable[1] = 4 + self.assertPrevious(name='retro', number=4, mutable=[1,2,3]) + + def test_post_save_changed(self): + self.update_instance(name='retro', number=4, mutable=[1,2,3]) + self.assertChanged() + self.instance.name = 'new age' + self.assertChanged(name='retro') + self.instance.number = 8 + self.assertChanged(name='retro', number=4) + self.instance.name = 'retro' + self.assertChanged(number=4) + self.instance.mutable[1] = 4 + self.assertChanged(number=4, mutable=[1,2,3]) + self.instance.mutable = [1,2,3] + self.assertChanged(number=4) + + def test_current(self): + self.assertCurrent(id=None, name='', number=None, mutable=None) + self.instance.name = 'new age' + self.assertCurrent(id=None, name='new age', number=None, mutable=None) + self.instance.number = 8 + self.assertCurrent(id=None, name='new age', number=8, mutable=None) + self.instance.mutable = [1,2,3] + self.assertCurrent(id=None, name='new age', number=8, mutable=[1,2,3]) + self.instance.mutable[1] = 4 + self.assertCurrent(id=None, name='new age', number=8, mutable=[1,4,3]) + self.instance.save() + self.assertCurrent(id=self.instance.id, name='new age', number=8, mutable=[1,4,3]) + + @skipUnless( + django.VERSION >= (1, 5, 0), "Django 1.4 doesn't have update_fields") + def test_update_fields(self): + self.update_instance(name='retro', number=4, mutable=[1,2,3]) + self.assertChanged() + self.instance.name = 'new age' + self.instance.number = 8 + self.instance.mutable = [4,5,6] + self.assertChanged(name='retro', number=4, mutable=[1,2,3]) + self.instance.save(update_fields=[]) + self.assertChanged(name='retro', number=4, mutable=[1,2,3]) + self.instance.save(update_fields=['name']) + in_db = self.tracked_class.objects.get(id=self.instance.id) + self.assertEqual(in_db.name, self.instance.name) + self.assertNotEqual(in_db.number, self.instance.number) + self.assertChanged(number=4, mutable=[1,2,3]) + self.instance.save(update_fields=['number']) + self.assertChanged(mutable=[1,2,3]) + self.instance.save(update_fields=['mutable']) + self.assertChanged() + in_db = self.tracked_class.objects.get(id=self.instance.id) + self.assertEqual(in_db.name, self.instance.name) + self.assertEqual(in_db.number, self.instance.number) + self.assertEqual(in_db.mutable, self.instance.mutable) + + def test_with_deferred(self): + self.instance.name = 'new age' + self.instance.number = 1 + self.instance.save() + item = list(self.tracked_class.objects.only('name').all())[0] + self.assertTrue(item._deferred_fields) + + self.assertEqual(item.tracker.previous('number'), None) + self.assertTrue('number' in item._deferred_fields) + + self.assertEqual(item.number, 1) + self.assertTrue('number' not in item._deferred_fields) + self.assertEqual(item.tracker.previous('number'), 1) + self.assertFalse(item.tracker.has_changed('number')) + + item.number = 2 + self.assertTrue(item.tracker.has_changed('number')) + + +class FieldTrackerMultipleInstancesTests(TestCase): + + def test_with_deferred_fields_access_multiple(self): + Tracked.objects.create(pk=1, name='foo', number=1) + Tracked.objects.create(pk=2, name='bar', number=2) + + queryset = Tracked.objects.only('id') + + for instance in queryset: + instance.name + + +class FieldTrackedModelCustomTests(FieldTrackerTestCase, + FieldTrackerCommonTests): + + tracked_class = TrackedNotDefault + + def setUp(self): + self.instance = self.tracked_class() + self.tracker = self.instance.name_tracker + + def test_pre_save_changed(self): + self.assertChanged(name=None) + self.instance.name = 'new age' + self.assertChanged(name=None) + self.instance.number = 8 + self.assertChanged(name=None) + self.instance.name = '' + self.assertChanged(name=None) + + def test_first_save(self): + self.assertHasChanged(name=True, number=None) + self.assertPrevious(name=None, number=None) + self.assertCurrent(name='') + self.assertChanged(name=None) + self.instance.name = 'retro' + self.instance.number = 4 + self.assertHasChanged(name=True, number=None) + self.assertPrevious(name=None, number=None) + self.assertCurrent(name='retro') + self.assertChanged(name=None) + + def test_pre_save_has_changed(self): + self.assertHasChanged(name=True, number=None) + self.instance.name = 'new age' + self.assertHasChanged(name=True, number=None) + self.instance.number = 7 + self.assertHasChanged(name=True, number=None) + + def test_post_save_has_changed(self): + self.update_instance(name='retro', number=4) + self.assertHasChanged(name=False, number=None) + self.instance.name = 'new age' + self.assertHasChanged(name=True, number=None) + self.instance.number = 8 + self.assertHasChanged(name=True, number=None) + self.instance.name = 'retro' + self.assertHasChanged(name=False, number=None) + + def test_post_save_previous(self): + self.update_instance(name='retro', number=4) + self.instance.name = 'new age' + self.assertPrevious(name='retro', number=None) + + def test_post_save_changed(self): + self.update_instance(name='retro', number=4) + self.assertChanged() + self.instance.name = 'new age' + self.assertChanged(name='retro') + self.instance.number = 8 + self.assertChanged(name='retro') + self.instance.name = 'retro' + self.assertChanged() + + def test_current(self): + self.assertCurrent(name='') + self.instance.name = 'new age' + self.assertCurrent(name='new age') + self.instance.number = 8 + self.assertCurrent(name='new age') + self.instance.save() + self.assertCurrent(name='new age') + + @skipUnless( + django.VERSION >= (1, 5, 0), "Django 1.4 doesn't have update_fields") + def test_update_fields(self): + self.update_instance(name='retro', number=4) + self.assertChanged() + self.instance.name = 'new age' + self.instance.number = 8 + self.instance.save(update_fields=['name', 'number']) + self.assertChanged() + + +class FieldTrackedModelAttributeTests(FieldTrackerTestCase): + + tracked_class = TrackedNonFieldAttr + + def setUp(self): + self.instance = self.tracked_class() + self.tracker = self.instance.tracker + + def test_previous(self): + self.assertPrevious(rounded=None) + self.instance.number = 7.5 + self.assertPrevious(rounded=None) + self.instance.save() + self.assertPrevious(rounded=8) + self.instance.number = 7.2 + self.assertPrevious(rounded=8) + self.instance.save() + self.assertPrevious(rounded=7) + + def test_has_changed(self): + self.assertHasChanged(rounded=False) + self.instance.number = 7.5 + self.assertHasChanged(rounded=True) + self.instance.save() + self.assertHasChanged(rounded=False) + self.instance.number = 7.2 + self.assertHasChanged(rounded=True) + self.instance.number = 7.8 + self.assertHasChanged(rounded=False) + + def test_changed(self): + self.assertChanged() + self.instance.number = 7.5 + self.assertPrevious(rounded=None) + self.instance.save() + self.assertPrevious() + self.instance.number = 7.8 + self.assertPrevious() + self.instance.number = 7.2 + self.assertPrevious(rounded=8) + self.instance.save() + self.assertPrevious() + + def test_current(self): + self.assertCurrent(rounded=None) + self.instance.number = 7.5 + self.assertCurrent(rounded=8) + self.instance.save() + self.assertCurrent(rounded=8) + + +class FieldTrackedModelMultiTests(FieldTrackerTestCase, + FieldTrackerCommonTests): + + tracked_class = TrackedMultiple + + def setUp(self): + self.instance = self.tracked_class() + self.trackers = [self.instance.name_tracker, + self.instance.number_tracker] + + def test_pre_save_changed(self): + self.tracker = self.instance.name_tracker + self.assertChanged(name=None) + self.instance.name = 'new age' + self.assertChanged(name=None) + self.instance.number = 8 + self.assertChanged(name=None) + self.instance.name = '' + self.assertChanged(name=None) + self.tracker = self.instance.number_tracker + self.assertChanged(number=None) + self.instance.name = 'new age' + self.assertChanged(number=None) + self.instance.number = 8 + self.assertChanged(number=None) + + def test_pre_save_has_changed(self): + self.tracker = self.instance.name_tracker + self.assertHasChanged(name=True, number=None) + self.instance.name = 'new age' + self.assertHasChanged(name=True, number=None) + self.tracker = self.instance.number_tracker + self.assertHasChanged(name=None, number=False) + self.instance.name = 'new age' + self.assertHasChanged(name=None, number=False) + + def test_pre_save_previous(self): + for tracker in self.trackers: + self.tracker = tracker + super(FieldTrackedModelMultiTests, self).test_pre_save_previous() + + def test_post_save_has_changed(self): + self.update_instance(name='retro', number=4) + self.assertHasChanged(tracker=self.trackers[0], name=False, number=None) + self.assertHasChanged(tracker=self.trackers[1], name=None, number=False) + self.instance.name = 'new age' + self.assertHasChanged(tracker=self.trackers[0], name=True, number=None) + self.assertHasChanged(tracker=self.trackers[1], name=None, number=False) + self.instance.number = 8 + self.assertHasChanged(tracker=self.trackers[0], name=True, number=None) + self.assertHasChanged(tracker=self.trackers[1], name=None, number=True) + self.instance.name = 'retro' + self.instance.number = 4 + self.assertHasChanged(tracker=self.trackers[0], name=False, number=None) + self.assertHasChanged(tracker=self.trackers[1], name=None, number=False) + + def test_post_save_previous(self): + self.update_instance(name='retro', number=4) + self.instance.name = 'new age' + self.instance.number = 8 + self.assertPrevious(tracker=self.trackers[0], name='retro', number=None) + self.assertPrevious(tracker=self.trackers[1], name=None, number=4) + + def test_post_save_changed(self): + self.update_instance(name='retro', number=4) + self.assertChanged(tracker=self.trackers[0]) + self.assertChanged(tracker=self.trackers[1]) + self.instance.name = 'new age' + self.assertChanged(tracker=self.trackers[0], name='retro') + self.assertChanged(tracker=self.trackers[1]) + self.instance.number = 8 + self.assertChanged(tracker=self.trackers[0], name='retro') + self.assertChanged(tracker=self.trackers[1], number=4) + self.instance.name = 'retro' + self.instance.number = 4 + self.assertChanged(tracker=self.trackers[0]) + self.assertChanged(tracker=self.trackers[1]) + + def test_current(self): + self.assertCurrent(tracker=self.trackers[0], name='') + self.assertCurrent(tracker=self.trackers[1], number=None) + self.instance.name = 'new age' + self.assertCurrent(tracker=self.trackers[0], name='new age') + self.assertCurrent(tracker=self.trackers[1], number=None) + self.instance.number = 8 + self.assertCurrent(tracker=self.trackers[0], name='new age') + self.assertCurrent(tracker=self.trackers[1], number=8) + self.instance.save() + self.assertCurrent(tracker=self.trackers[0], name='new age') + self.assertCurrent(tracker=self.trackers[1], number=8) + + +class FieldTrackerForeignKeyTests(FieldTrackerTestCase): + + fk_class = Tracked + tracked_class = TrackedFK + + def setUp(self): + self.old_fk = self.fk_class.objects.create(number=8) + self.instance = self.tracked_class.objects.create(fk=self.old_fk) + + def test_default(self): + self.tracker = self.instance.tracker + self.assertChanged() + self.assertPrevious() + self.assertCurrent(id=self.instance.id, fk_id=self.old_fk.id) + self.instance.fk = self.fk_class.objects.create(number=8) + self.assertChanged(fk_id=self.old_fk.id) + self.assertPrevious(fk_id=self.old_fk.id) + self.assertCurrent(id=self.instance.id, fk_id=self.instance.fk_id) + + def test_custom(self): + self.tracker = self.instance.custom_tracker + self.assertChanged() + self.assertPrevious() + self.assertCurrent(fk_id=self.old_fk.id) + self.instance.fk = self.fk_class.objects.create(number=8) + self.assertChanged(fk_id=self.old_fk.id) + self.assertPrevious(fk_id=self.old_fk.id) + self.assertCurrent(fk_id=self.instance.fk_id) + + def test_custom_without_id(self): + with self.assertNumQueries(1): + self.tracked_class.objects.get() + self.tracker = self.instance.custom_tracker_without_id + self.assertChanged() + self.assertPrevious() + self.assertCurrent(fk=self.old_fk.id) + self.instance.fk = self.fk_class.objects.create(number=8) + self.assertChanged(fk=self.old_fk.id) + self.assertPrevious(fk=self.old_fk.id) + self.assertCurrent(fk=self.instance.fk_id) + + +class InheritedFieldTrackerTests(FieldTrackerTests): + + tracked_class = InheritedTracked + + def test_child_fields_not_tracked(self): + self.name2 = 'test' + self.assertEqual(self.tracker.previous('name2'), None) + self.assertRaises(FieldError, self.tracker.has_changed, 'name2') + + +class FieldTrackerInheritedForeignKeyTests(FieldTrackerForeignKeyTests): + + tracked_class = InheritedTrackedFK + + +class FieldTrackerFileFieldTests(FieldTrackerTestCase): + + tracked_class = TrackedFileField + + def setUp(self): + self.instance = self.tracked_class() + self.tracker = self.instance.tracker + self.some_file = 'something.txt' + self.another_file = 'another.txt' + + def test_pre_save_changed(self): + self.assertChanged(some_file=None) + self.instance.some_file = self.some_file + self.assertChanged(some_file=None) + + def test_pre_save_has_changed(self): + self.assertHasChanged(some_file=True) + self.instance.some_file = self.some_file + self.assertHasChanged(some_file=True) + + def test_pre_save_previous(self): + self.assertPrevious(some_file=None) + self.instance.some_file = self.some_file + self.assertPrevious(some_file=None) + + def test_post_save_changed(self): + self.update_instance(some_file=self.some_file) + self.assertChanged() + previous_file = self.instance.some_file + self.instance.some_file = self.another_file + self.assertChanged(some_file=previous_file) + # test deferred file field + deferred_instance = self.tracked_class.objects.defer('some_file')[0] + deferred_instance.some_file # access field to fetch from database + self.assertChanged(tracker=deferred_instance.tracker) + + previous_file = deferred_instance.some_file + deferred_instance.some_file = self.another_file + self.assertChanged( + tracker=deferred_instance.tracker, + some_file=previous_file, + ) + + def test_post_save_has_changed(self): + self.update_instance(some_file=self.some_file) + self.assertHasChanged(some_file=False) + self.instance.some_file = self.another_file + self.assertHasChanged(some_file=True) + + # test deferred file field + deferred_instance = self.tracked_class.objects.defer('some_file')[0] + deferred_instance.some_file # access field to fetch from database + self.assertHasChanged( + tracker=deferred_instance.tracker, + some_file=False, + ) + + deferred_instance.some_file = self.another_file + self.assertHasChanged( + tracker=deferred_instance.tracker, + some_file=True, + ) + + def test_post_save_previous(self): + self.update_instance(some_file=self.some_file) + previous_file = self.instance.some_file + self.instance.some_file = self.another_file + self.assertPrevious(some_file=previous_file) + + # test deferred file field + deferred_instance = self.tracked_class.objects.defer('some_file')[0] + deferred_instance.some_file # access field to fetch from database + self.assertPrevious( + tracker=deferred_instance.tracker, + some_file=previous_file, + ) + + deferred_instance.some_file = self.another_file + self.assertPrevious( + tracker=deferred_instance.tracker, + some_file=previous_file, + ) + + def test_current(self): + self.assertCurrent(some_file=self.instance.some_file, id=None) + self.instance.some_file = self.some_file + self.assertCurrent(some_file=self.instance.some_file, id=None) + + # test deferred file field + self.instance.save() + deferred_instance = self.tracked_class.objects.defer('some_file')[0] + deferred_instance.some_file # access field to fetch from database + self.assertCurrent( + some_file=self.instance.some_file, + id=self.instance.id, + ) + + self.instance.some_file = self.another_file + self.assertCurrent( + some_file=self.instance.some_file, + id=self.instance.id, + ) diff --git a/model_utils/tests/test_fields/test_monitor_field.py b/model_utils/tests/test_fields/test_monitor_field.py new file mode 100644 index 0000000..779f502 --- /dev/null +++ b/model_utils/tests/test_fields/test_monitor_field.py @@ -0,0 +1,120 @@ +from __future__ import unicode_literals + +from datetime import datetime + +from freezegun import freeze_time + +from django.test import TestCase + +from model_utils.fields import MonitorField +from model_utils.tests.models import Monitored, MonitorWhen, MonitorWhenEmpty, DoubleMonitored + + +class MonitorFieldTests(TestCase): + def setUp(self): + with freeze_time(datetime(2016, 1, 1, 10, 0, 0)): + self.instance = Monitored(name='Charlie') + self.created = self.instance.name_changed + + def test_save_no_change(self): + self.instance.save() + self.assertEqual(self.instance.name_changed, self.created) + + def test_save_changed(self): + with freeze_time(datetime(2016, 1, 1, 12, 0, 0)): + self.instance.name = 'Maria' + self.instance.save() + self.assertEqual(self.instance.name_changed, datetime(2016, 1, 1, 12, 0, 0)) + + def test_double_save(self): + self.instance.name = 'Jose' + self.instance.save() + changed = self.instance.name_changed + self.instance.save() + self.assertEqual(self.instance.name_changed, changed) + + def test_no_monitor_arg(self): + with self.assertRaises(TypeError): + MonitorField() + + +class MonitorWhenFieldTests(TestCase): + """ + Will record changes only when name is 'Jose' or 'Maria' + """ + def setUp(self): + with freeze_time(datetime(2016, 1, 1, 10, 0, 0)): + self.instance = MonitorWhen(name='Charlie') + self.created = self.instance.name_changed + + def test_save_no_change(self): + self.instance.save() + self.assertEqual(self.instance.name_changed, self.created) + + def test_save_changed_to_Jose(self): + with freeze_time(datetime(2016, 1, 1, 12, 0, 0)): + self.instance.name = 'Jose' + self.instance.save() + self.assertEqual(self.instance.name_changed, datetime(2016, 1, 1, 12, 0, 0)) + + def test_save_changed_to_Maria(self): + with freeze_time(datetime(2016, 1, 1, 12, 0, 0)): + self.instance.name = 'Maria' + self.instance.save() + self.assertEqual(self.instance.name_changed, datetime(2016, 1, 1, 12, 0, 0)) + + def test_save_changed_to_Pedro(self): + self.instance.name = 'Pedro' + self.instance.save() + self.assertEqual(self.instance.name_changed, self.created) + + def test_double_save(self): + self.instance.name = 'Jose' + self.instance.save() + changed = self.instance.name_changed + self.instance.save() + self.assertEqual(self.instance.name_changed, changed) + + +class MonitorWhenEmptyFieldTests(TestCase): + """ + Monitor should never be updated id when is an empty list. + """ + def setUp(self): + self.instance = MonitorWhenEmpty(name='Charlie') + self.created = self.instance.name_changed + + def test_save_no_change(self): + self.instance.save() + self.assertEqual(self.instance.name_changed, self.created) + + def test_save_changed_to_Jose(self): + self.instance.name = 'Jose' + self.instance.save() + self.assertEqual(self.instance.name_changed, self.created) + + def test_save_changed_to_Maria(self): + self.instance.name = 'Maria' + self.instance.save() + self.assertEqual(self.instance.name_changed, self.created) + + +class MonitorDoubleFieldTests(TestCase): + + def setUp(self): + DoubleMonitored.objects.create(name='Charlie', name2='Charlie2') + + def test_recursion_error_with_only(self): + # Any field passed to only() is generating a recursion error + list(DoubleMonitored.objects.only('id')) + + def test_recursion_error_with_defer(self): + # Only monitored fields passed to defer() are failing + list(DoubleMonitored.objects.defer('name')) + + def test_monitor_still_works_with_deferred_fields_filtered_out_of_save_initial(self): + obj = DoubleMonitored.objects.defer('name').get(name='Charlie') + with freeze_time("2016-12-01"): + obj.name = 'Charlie2' + obj.save() + self.assertEqual(obj.name_changed, datetime(2016, 12, 1)) diff --git a/model_utils/tests/test_fields/test_split_field.py b/model_utils/tests/test_fields/test_split_field.py new file mode 100644 index 0000000..57802fd --- /dev/null +++ b/model_utils/tests/test_fields/test_split_field.py @@ -0,0 +1,78 @@ +from __future__ import unicode_literals + +from django.utils.six import text_type +from django.test import TestCase + +from model_utils.tests.models import Article, SplitFieldAbstractParent + + +class SplitFieldTests(TestCase): + full_text = 'summary\n\n\n\nmore' + excerpt = 'summary\n' + + def setUp(self): + self.post = Article.objects.create( + title='example post', body=self.full_text) + + def test_unicode_content(self): + self.assertEqual(text_type(self.post.body), self.full_text) + + def test_excerpt(self): + self.assertEqual(self.post.body.excerpt, self.excerpt) + + def test_content(self): + self.assertEqual(self.post.body.content, self.full_text) + + def test_has_more(self): + self.assertTrue(self.post.body.has_more) + + def test_not_has_more(self): + post = Article.objects.create(title='example 2', + body='some text\n\nsome more\n') + self.assertFalse(post.body.has_more) + + def test_load_back(self): + post = Article.objects.get(pk=self.post.pk) + self.assertEqual(post.body.content, self.post.body.content) + self.assertEqual(post.body.excerpt, self.post.body.excerpt) + + def test_assign_to_body(self): + new_text = 'different\n\n\n\nother' + self.post.body = new_text + self.post.save() + self.assertEqual(text_type(self.post.body), new_text) + + def test_assign_to_content(self): + new_text = 'different\n\n\n\nother' + self.post.body.content = new_text + self.post.save() + self.assertEqual(text_type(self.post.body), new_text) + + def test_assign_to_excerpt(self): + with self.assertRaises(AttributeError): + self.post.body.excerpt = 'this should fail' + + def test_access_via_class(self): + with self.assertRaises(AttributeError): + Article.body + + def test_none(self): + a = Article(title='Some Title', body=None) + self.assertEqual(a.body, None) + + def test_assign_splittext(self): + a = Article(title='Some Title') + a.body = self.post.body + self.assertEqual(a.body.excerpt, 'summary\n') + + def test_value_to_string(self): + f = self.post._meta.get_field('body') + self.assertEqual(f.value_to_string(self.post), self.full_text) + + def test_abstract_inheritance(self): + class Child(SplitFieldAbstractParent): + pass + + self.assertEqual( + [f.name for f in Child._meta.fields], + ["id", "content", "_content_excerpt"]) diff --git a/model_utils/tests/test_fields/test_status_field.py b/model_utils/tests/test_fields/test_status_field.py new file mode 100644 index 0000000..73dabac --- /dev/null +++ b/model_utils/tests/test_fields/test_status_field.py @@ -0,0 +1,32 @@ +from __future__ import unicode_literals + +from django.test import TestCase + +from model_utils.fields import StatusField +from model_utils.tests.models import ( + Article, StatusFieldDefaultFilled, StatusFieldDefaultNotFilled, + StatusFieldChoicesName, + ) + + +class StatusFieldTests(TestCase): + + def test_status_with_default_filled(self): + instance = StatusFieldDefaultFilled() + self.assertEqual(instance.status, instance.STATUS.yes) + + def test_status_with_default_not_filled(self): + instance = StatusFieldDefaultNotFilled() + self.assertEqual(instance.status, instance.STATUS.no) + + def test_no_check_for_status(self): + field = StatusField(no_check_for_status=True) + # this model has no STATUS attribute, so checking for it would error + field.prepare_class(Article) + + def test_get_status_display(self): + instance = StatusFieldDefaultFilled() + self.assertEqual(instance.get_status_display(), "Yes") + + def test_choices_name(self): + StatusFieldChoicesName() diff --git a/model_utils/tests/test_managers/__init__.py b/model_utils/tests/test_managers/__init__.py new file mode 100644 index 0000000..0f8aec6 --- /dev/null +++ b/model_utils/tests/test_managers/__init__.py @@ -0,0 +1,4 @@ +# Needed for Django 1.4/1.5 test runner +from .test_inheritance_manager import * +from .test_query_manager import * +from .test_status_manager import * diff --git a/model_utils/tests/test_managers/test_inheritance_manager.py b/model_utils/tests/test_managers/test_inheritance_manager.py new file mode 100644 index 0000000..65d8b59 --- /dev/null +++ b/model_utils/tests/test_managers/test_inheritance_manager.py @@ -0,0 +1,500 @@ +from __future__ import unicode_literals + +import django +from django.db import models +from django.test import TestCase + +from model_utils.tests.helpers import skipUnless +from model_utils.tests.models import (InheritanceManagerTestRelated, InheritanceManagerTestGrandChild1, + InheritanceManagerTestGrandChild1_2, InheritanceManagerTestParent, + InheritanceManagerTestChild1, + InheritanceManagerTestChild2, TimeFrame, InheritanceManagerTestChild3 + ) + + +class InheritanceManagerTests(TestCase): + def setUp(self): + self.child1 = InheritanceManagerTestChild1.objects.create() + self.child2 = InheritanceManagerTestChild2.objects.create() + self.grandchild1 = InheritanceManagerTestGrandChild1.objects.create() + self.grandchild1_2 = \ + InheritanceManagerTestGrandChild1_2.objects.create() + + def get_manager(self): + return InheritanceManagerTestParent.objects + + def test_normal(self): + children = set([ + InheritanceManagerTestParent(pk=self.child1.pk), + InheritanceManagerTestParent(pk=self.child2.pk), + InheritanceManagerTestParent(pk=self.grandchild1.pk), + InheritanceManagerTestParent(pk=self.grandchild1_2.pk), + ]) + self.assertEqual(set(self.get_manager().all()), children) + + def test_select_all_subclasses(self): + children = set([self.child1, self.child2]) + if django.VERSION >= (1, 6, 0): + children.add(self.grandchild1) + children.add(self.grandchild1_2) + else: + children.add(InheritanceManagerTestChild1(pk=self.grandchild1.pk)) + children.add(InheritanceManagerTestChild1(pk=self.grandchild1_2.pk)) + self.assertEqual( + set(self.get_manager().select_subclasses()), children) + + def test_select_subclasses_invalid_relation(self): + """ + If an invalid relation string is provided, we can provide the user + with a list which is valid, rather than just have the select_related() + raise an AttributeError further in. + """ + regex = '^.+? is not in the discovered subclasses, tried:.+$' + with self.assertRaisesRegexp(ValueError, regex): + self.get_manager().select_subclasses('user') + + def test_select_specific_subclasses(self): + children = set([ + self.child1, + InheritanceManagerTestParent(pk=self.child2.pk), + InheritanceManagerTestChild1(pk=self.grandchild1.pk), + InheritanceManagerTestChild1(pk=self.grandchild1_2.pk), + ]) + self.assertEqual( + set( + self.get_manager().select_subclasses( + "inheritancemanagertestchild1") + ), + children, + ) + + @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") + def test_select_specific_grandchildren(self): + children = set([ + InheritanceManagerTestParent(pk=self.child1.pk), + InheritanceManagerTestParent(pk=self.child2.pk), + self.grandchild1, + InheritanceManagerTestParent(pk=self.grandchild1_2.pk), + ]) + self.assertEqual( + set( + self.get_manager().select_subclasses( + "inheritancemanagertestchild1__inheritancemanagertestgrandchild1" + ) + ), + children, + ) + + @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") + def test_children_and_grandchildren(self): + children = set([ + self.child1, + InheritanceManagerTestParent(pk=self.child2.pk), + self.grandchild1, + InheritanceManagerTestChild1(pk=self.grandchild1_2.pk), + ]) + self.assertEqual( + set( + self.get_manager().select_subclasses( + "inheritancemanagertestchild1", + "inheritancemanagertestchild1__inheritancemanagertestgrandchild1" + ) + ), + children, + ) + + def test_get_subclass(self): + self.assertEqual( + self.get_manager().get_subclass(pk=self.child1.pk), + self.child1) + + def test_get_subclass_on_queryset(self): + self.assertEqual( + self.get_manager().all().get_subclass(pk=self.child1.pk), + self.child1) + + def test_prior_select_related(self): + with self.assertNumQueries(1): + obj = self.get_manager().select_related( + "inheritancemanagertestchild1").select_subclasses( + "inheritancemanagertestchild2").get(pk=self.child1.pk) + obj.inheritancemanagertestchild1 + + @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") + def test_version_determining_any_depth(self): + self.assertIsNone(self.get_manager().all()._get_maximum_depth()) + + @skipUnless(django.VERSION < (1, 6, 0), "test only applies to Django < 1.6") + def test_version_determining_only_child_depth(self): + self.assertEqual(1, self.get_manager().all()._get_maximum_depth()) + + @skipUnless(django.VERSION < (1, 6, 0), "test only applies to Django < 1.6") + def test_manually_specifying_parent_fk_only_children(self): + """ + given a Model which inherits from another Model, but also declares + the OneToOne link manually using `related_name` and `parent_link`, + ensure that the relation names and subclasses are obtained correctly. + """ + child3 = InheritanceManagerTestChild3.objects.create() + results = InheritanceManagerTestParent.objects.all().select_subclasses() + + expected_objs = [self.child1, self.child2, + InheritanceManagerTestChild1(pk=self.grandchild1.pk), + InheritanceManagerTestChild1(pk=self.grandchild1_2.pk), + child3] + self.assertEqual(list(results), expected_objs) + + expected_related_names = [ + 'inheritancemanagertestchild1', + 'inheritancemanagertestchild2', + 'manual_onetoone', # this was set via parent_link & related_name + ] + self.assertEqual(set(results.subclasses), + set(expected_related_names)) + + @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") + def test_manually_specifying_parent_fk_including_grandchildren(self): + """ + given a Model which inherits from another Model, but also declares + the OneToOne link manually using `related_name` and `parent_link`, + ensure that the relation names and subclasses are obtained correctly. + """ + child3 = InheritanceManagerTestChild3.objects.create() + results = InheritanceManagerTestParent.objects.all().select_subclasses() + + expected_objs = [self.child1, self.child2, self.grandchild1, + self.grandchild1_2, child3] + self.assertEqual(list(results), expected_objs) + + expected_related_names = [ + 'inheritancemanagertestchild1__inheritancemanagertestgrandchild1', + 'inheritancemanagertestchild1__inheritancemanagertestgrandchild1_2', + 'inheritancemanagertestchild1', + 'inheritancemanagertestchild2', + 'manual_onetoone', # this was set via parent_link & related_name + ] + self.assertEqual(set(results.subclasses), + set(expected_related_names)) + + def test_manually_specifying_parent_fk_single_subclass(self): + """ + Using a string related_name when the relation is manually defined + instead of implicit should still work in the same way. + """ + related_name = 'manual_onetoone' + child3 = InheritanceManagerTestChild3.objects.create() + results = InheritanceManagerTestParent.objects.all().select_subclasses(related_name) + + expected_objs = [InheritanceManagerTestParent(pk=self.child1.pk), + InheritanceManagerTestParent(pk=self.child2.pk), + InheritanceManagerTestParent(pk=self.grandchild1.pk), + InheritanceManagerTestParent(pk=self.grandchild1_2.pk), + child3] + self.assertEqual(list(results), expected_objs) + expected_related_names = [related_name] + self.assertEqual(set(results.subclasses), + set(expected_related_names)) + + def test_filter_on_values_queryset(self): + queryset = InheritanceManagerTestChild1.objects.values('id').filter(pk=self.child1.pk) + self.assertEqual(list(queryset), [{'id': self.child1.pk}]) + + @skipUnless(django.VERSION >= (1, 9, 0), "test only applies to Django 1.9+") + def test_dj19_values_list_on_select_subclasses(self): + """ + Using `select_subclasses` in conjunction with `values_list()` raised an + exception in `_get_sub_obj_recurse()` because the result of `values_list()` + is either a `tuple` or primitive objects if `flat=True` is specified, + because no type checking was done prior to fetching child nodes. + + Django versions below 1.9 are not affected by this bug. + """ + + # Querysets are cast to lists to force immediate evaluation. + # No exceptions must be thrown. + + # No argument to select_subclasses + objs_1 = list( + self.get_manager(). + select_subclasses(). + values_list('id') + ) + + # String argument to select_subclasses + objs_2 = list( + self.get_manager(). + select_subclasses( + "inheritancemanagertestchild2" + ). + values_list('id') + ) + + # String argument to select_subclasses + objs_3 = list( + self.get_manager(). + select_subclasses( + InheritanceManagerTestChild2 + ). + values_list('id') + ) + + assert all(( + isinstance(objs_1, list), + isinstance(objs_2, list), + isinstance(objs_3, list), + )) + + assert objs_1 == objs_2 == objs_3 + + +class InheritanceManagerUsingModelsTests(TestCase): + def setUp(self): + self.parent1 = InheritanceManagerTestParent.objects.create() + self.child1 = InheritanceManagerTestChild1.objects.create() + self.child2 = InheritanceManagerTestChild2.objects.create() + self.grandchild1 = InheritanceManagerTestGrandChild1.objects.create() + self.grandchild1_2 = InheritanceManagerTestGrandChild1_2.objects.create() + + def test_select_subclass_by_child_model(self): + """ + Confirm that passing a child model works the same as passing the + select_related manually + """ + objs = InheritanceManagerTestParent.objects.select_subclasses( + "inheritancemanagertestchild1").order_by('pk') + objsmodels = InheritanceManagerTestParent.objects.select_subclasses( + InheritanceManagerTestChild1).order_by('pk') + self.assertEqual(objs.subclasses, objsmodels.subclasses) + self.assertEqual(list(objs), list(objsmodels)) + + @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") + def test_select_subclass_by_grandchild_model(self): + """ + Confirm that passing a grandchild model works the same as passing the + select_related manually + """ + objs = InheritanceManagerTestParent.objects.select_subclasses( + "inheritancemanagertestchild1__inheritancemanagertestgrandchild1") \ + .order_by('pk') + objsmodels = InheritanceManagerTestParent.objects.select_subclasses( + InheritanceManagerTestGrandChild1).order_by('pk') + self.assertEqual(objs.subclasses, objsmodels.subclasses) + self.assertEqual(list(objs), list(objsmodels)) + + @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") + def test_selecting_all_subclasses_specifically_grandchildren(self): + """ + A bare select_subclasses() should achieve the same results as doing + select_subclasses and specifying all possible subclasses. + This test checks grandchildren, so only works on 1.6>= + """ + objs = InheritanceManagerTestParent.objects.select_subclasses().order_by('pk') + objsmodels = InheritanceManagerTestParent.objects.select_subclasses( + InheritanceManagerTestChild1, InheritanceManagerTestChild2, + InheritanceManagerTestChild3, + InheritanceManagerTestGrandChild1, + InheritanceManagerTestGrandChild1_2).order_by('pk') + self.assertEqual(set(objs.subclasses), set(objsmodels.subclasses)) + self.assertEqual(list(objs), list(objsmodels)) + + def test_selecting_all_subclasses_specifically_children(self): + """ + A bare select_subclasses() should achieve the same results as doing + select_subclasses and specifying all possible subclasses. + + Note: This is sort of the same test as + `test_selecting_all_subclasses_specifically_grandchildren` but it + specifically switches what models are used because that happens + behind the scenes in a bare select_subclasses(), so we need to + emulate it. + """ + objs = InheritanceManagerTestParent.objects.select_subclasses().order_by('pk') + + if django.VERSION >= (1, 6, 0): + models = (InheritanceManagerTestChild1, + InheritanceManagerTestChild2, + InheritanceManagerTestChild3, + InheritanceManagerTestGrandChild1, + InheritanceManagerTestGrandChild1_2) + else: + models = (InheritanceManagerTestChild1, + InheritanceManagerTestChild2, + InheritanceManagerTestChild3) + + objsmodels = InheritanceManagerTestParent.objects.select_subclasses( + *models).order_by('pk') + # order shouldn't matter, I don't think, as long as the resulting + # queryset (when cast to a list) is the same. + self.assertEqual(set(objs.subclasses), set(objsmodels.subclasses)) + self.assertEqual(list(objs), list(objsmodels)) + + def test_select_subclass_just_self(self): + """ + Passing in the same model as the manager/queryset is bound against + (ie: the root parent) should have no effect on the result set. + """ + objsmodels = InheritanceManagerTestParent.objects.select_subclasses( + InheritanceManagerTestParent).order_by('pk') + self.assertEqual([], objsmodels.subclasses) + self.assertEqual(list(objsmodels), [ + InheritanceManagerTestParent(pk=self.parent1.pk), + InheritanceManagerTestParent(pk=self.child1.pk), + InheritanceManagerTestParent(pk=self.child2.pk), + InheritanceManagerTestParent(pk=self.grandchild1.pk), + InheritanceManagerTestParent(pk=self.grandchild1_2.pk), + ]) + + def test_select_subclass_invalid_related_model(self): + """ + Confirming that giving a stupid model doesn't work. + """ + regex = '^.+? is not a subclass of .+$' + with self.assertRaisesRegexp(ValueError, regex): + InheritanceManagerTestParent.objects.select_subclasses( + TimeFrame).order_by('pk') + + @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") + def test_mixing_strings_and_classes_with_grandchildren(self): + """ + Given arguments consisting of both strings and model classes, + ensure the right resolutions take place, accounting for the extra + depth (grandchildren etc) 1.6> allows. + """ + objs = InheritanceManagerTestParent.objects.select_subclasses( + "inheritancemanagertestchild2", + InheritanceManagerTestGrandChild1_2).order_by('pk') + expecting = ['inheritancemanagertestchild1__inheritancemanagertestgrandchild1_2', + 'inheritancemanagertestchild2'] + self.assertEqual(set(objs.subclasses), set(expecting)) + expecting2 = [ + InheritanceManagerTestParent(pk=self.parent1.pk), + InheritanceManagerTestParent(pk=self.child1.pk), + InheritanceManagerTestChild2(pk=self.child2.pk), + InheritanceManagerTestParent(pk=self.grandchild1.pk), + InheritanceManagerTestGrandChild1_2(pk=self.grandchild1_2.pk), + ] + self.assertEqual(list(objs), expecting2) + + def test_mixing_strings_and_classes_with_children(self): + """ + Given arguments consisting of both strings and model classes, + ensure the right resolutions take place, walking down as far as + children. + """ + objs = InheritanceManagerTestParent.objects.select_subclasses( + "inheritancemanagertestchild2", + InheritanceManagerTestChild1).order_by('pk') + expecting = ['inheritancemanagertestchild1', + 'inheritancemanagertestchild2'] + + self.assertEqual(set(objs.subclasses), set(expecting)) + expecting2 = [ + InheritanceManagerTestParent(pk=self.parent1.pk), + InheritanceManagerTestChild1(pk=self.child1.pk), + InheritanceManagerTestChild2(pk=self.child2.pk), + InheritanceManagerTestChild1(pk=self.grandchild1.pk), + InheritanceManagerTestChild1(pk=self.grandchild1_2.pk), + ] + self.assertEqual(list(objs), expecting2) + + def test_duplications(self): + """ + Check that even if the same thing is provided as a string and a model + that the right results are retrieved. + """ + # mixing strings and models which evaluate to the same thing is fine. + objs = InheritanceManagerTestParent.objects.select_subclasses( + "inheritancemanagertestchild2", + InheritanceManagerTestChild2).order_by('pk') + self.assertEqual(list(objs), [ + InheritanceManagerTestParent(pk=self.parent1.pk), + InheritanceManagerTestParent(pk=self.child1.pk), + InheritanceManagerTestChild2(pk=self.child2.pk), + InheritanceManagerTestParent(pk=self.grandchild1.pk), + InheritanceManagerTestParent(pk=self.grandchild1_2.pk), + ]) + + @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") + def test_child_doesnt_accidentally_get_parent(self): + """ + Given a Child model which also has an InheritanceManager, + none of the returned objects should be Parent objects. + """ + objs = InheritanceManagerTestChild1.objects.select_subclasses( + InheritanceManagerTestGrandChild1).order_by('pk') + self.assertEqual([ + InheritanceManagerTestChild1(pk=self.child1.pk), + InheritanceManagerTestGrandChild1(pk=self.grandchild1.pk), + InheritanceManagerTestChild1(pk=self.grandchild1_2.pk), + ], list(objs)) + + def test_manually_specifying_parent_fk_only_specific_child(self): + """ + given a Model which inherits from another Model, but also declares + the OneToOne link manually using `related_name` and `parent_link`, + ensure that the relation names and subclasses are obtained correctly. + """ + child3 = InheritanceManagerTestChild3.objects.create() + results = InheritanceManagerTestParent.objects.all().select_subclasses( + InheritanceManagerTestChild3) + + expected_objs = [InheritanceManagerTestParent(pk=self.parent1.pk), + InheritanceManagerTestParent(pk=self.child1.pk), + InheritanceManagerTestParent(pk=self.child2.pk), + InheritanceManagerTestParent(pk=self.grandchild1.pk), + InheritanceManagerTestParent(pk=self.grandchild1_2.pk), + child3] + self.assertEqual(list(results), expected_objs) + + expected_related_names = ['manual_onetoone'] + self.assertEqual(set(results.subclasses), + set(expected_related_names)) + + def test_extras_descend(self): + """ + Ensure that extra(select=) values are copied onto sub-classes. + """ + results = InheritanceManagerTestParent.objects.select_subclasses().extra( + select={'foo': 'id + 1'} + ) + self.assertTrue(all(result.foo == (result.id + 1) for result in results)) + + +class InheritanceManagerRelatedTests(InheritanceManagerTests): + def setUp(self): + self.related = InheritanceManagerTestRelated.objects.create() + self.child1 = InheritanceManagerTestChild1.objects.create( + related=self.related) + self.child2 = InheritanceManagerTestChild2.objects.create( + related=self.related) + self.grandchild1 = InheritanceManagerTestGrandChild1.objects.create(related=self.related) + self.grandchild1_2 = InheritanceManagerTestGrandChild1_2.objects.create(related=self.related) + + def get_manager(self): + return self.related.imtests + + def test_get_method_with_select_subclasses(self): + self.assertEqual( + InheritanceManagerTestParent.objects.select_subclasses().get( + id=self.child1.id), + self.child1) + + def test_annotate_with_select_subclasses(self): + qs = InheritanceManagerTestParent.objects.select_subclasses().annotate( + models.Count('id')) + self.assertEqual(qs.get(id=self.child1.id).id__count, 1) + + def test_annotate_with_named_arguments_with_select_subclasses(self): + qs = InheritanceManagerTestParent.objects.select_subclasses().annotate( + test_count=models.Count('id')) + self.assertEqual(qs.get(id=self.child1.id).test_count, 1) + + def test_annotate_before_select_subclasses(self): + qs = InheritanceManagerTestParent.objects.annotate( + models.Count('id')).select_subclasses() + self.assertEqual(qs.get(id=self.child1.id).id__count, 1) + + def test_annotate_with_named_arguments_before_select_subclasses(self): + qs = InheritanceManagerTestParent.objects.annotate( + test_count=models.Count('id')).select_subclasses() + self.assertEqual(qs.get(id=self.child1.id).test_count, 1) diff --git a/model_utils/tests/test_managers/test_query_manager.py b/model_utils/tests/test_managers/test_query_manager.py new file mode 100644 index 0000000..70f2f46 --- /dev/null +++ b/model_utils/tests/test_managers/test_query_manager.py @@ -0,0 +1,29 @@ +from __future__ import unicode_literals + +from django.test import TestCase + +from model_utils.tests.models import Post + + +class QueryManagerTests(TestCase): + def setUp(self): + data = ((True, True, 0), + (True, False, 4), + (False, False, 2), + (False, True, 3), + (True, True, 1), + (True, False, 5)) + for p, c, o in data: + Post.objects.create(published=p, confirmed=c, order=o) + + def test_passing_kwargs(self): + qs = Post.public.all() + self.assertEqual([p.order for p in qs], [0, 1, 4, 5]) + + def test_passing_Q(self): + qs = Post.public_confirmed.all() + self.assertEqual([p.order for p in qs], [0, 1]) + + def test_ordering(self): + qs = Post.public_reversed.all() + self.assertEqual([p.order for p in qs], [5, 4, 1, 0]) diff --git a/model_utils/tests/test_managers/test_status_manager.py b/model_utils/tests/test_managers/test_status_manager.py new file mode 100644 index 0000000..af3d7cb --- /dev/null +++ b/model_utils/tests/test_managers/test_status_manager.py @@ -0,0 +1,23 @@ +from __future__ import unicode_literals + +from django.db import models +from django.core.exceptions import ImproperlyConfigured +from django.test import TestCase + +from model_utils.managers import QueryManager +from model_utils.models import StatusModel +from model_utils.tests.models import StatusManagerAdded + + +class StatusManagerAddedTests(TestCase): + def test_manager_available(self): + self.assertTrue(isinstance(StatusManagerAdded.active, QueryManager)) + + def test_conflict_error(self): + with self.assertRaises(ImproperlyConfigured): + class ErrorModel(StatusModel): + STATUS = ( + ('active', 'Is Active'), + ('deleted', 'Is Deleted'), + ) + active = models.BooleanField() diff --git a/model_utils/tests/test_miscellaneous.py b/model_utils/tests/test_miscellaneous.py new file mode 100644 index 0000000..8b1df05 --- /dev/null +++ b/model_utils/tests/test_miscellaneous.py @@ -0,0 +1,60 @@ +from __future__ import unicode_literals + +import django +from django.db.models.fields import FieldDoesNotExist +from django.core.management import call_command +from django.test import TestCase + +from model_utils.fields import get_excerpt +from model_utils.tests.models import ( + Article, + StatusFieldDefaultFilled, +) +from model_utils.tests.helpers import skipUnless + + +class MigrationsTests(TestCase): + @skipUnless(django.VERSION >= (1, 7, 0), "test only applies to Django 1.7+") + def test_makemigrations(self): + call_command('makemigrations', dry_run=True) + + +class GetExcerptTests(TestCase): + def test_split(self): + e = get_excerpt("some content\n\n\n\nsome more") + self.assertEqual(e, 'some content\n') + + def test_auto_split(self): + e = get_excerpt("para one\n\npara two\n\npara three") + self.assertEqual(e, 'para one\n\npara two') + + def test_middle_of_para(self): + e = get_excerpt("some text\n\nmore text") + self.assertEqual(e, 'some text') + + def test_middle_of_line(self): + e = get_excerpt("some text more text") + self.assertEqual(e, "some text more text") + +try: + from south.modelsinspector import introspector +except ImportError: + introspector = None + + +@skipUnless(introspector, 'South is not installed') +class SouthFreezingTests(TestCase): + def test_introspector_adds_no_excerpt_field(self): + mf = Article._meta.get_field('body') + args, kwargs = introspector(mf) + self.assertEqual(kwargs['no_excerpt_field'], 'True') + + def test_no_excerpt_field_works(self): + from .models import NoRendered + with self.assertRaises(FieldDoesNotExist): + NoRendered._meta.get_field('_body_excerpt') + + def test_status_field_no_check_for_status(self): + sf = StatusFieldDefaultFilled._meta.get_field('status') + args, kwargs = introspector(sf) + self.assertEqual(kwargs['no_check_for_status'], 'True') diff --git a/model_utils/tests/test_models/__init__.py b/model_utils/tests/test_models/__init__.py new file mode 100644 index 0000000..5065567 --- /dev/null +++ b/model_utils/tests/test_models/__init__.py @@ -0,0 +1,6 @@ +# Needed for Django 1.4/1.5 test runner +from .test_model_tracker import * +from .test_softdeletable_model import * +from .test_status_model import * +from .test_timeframed_model import * +from .test_timestamped_model import * diff --git a/model_utils/tests/test_models/test_model_tracker.py b/model_utils/tests/test_models/test_model_tracker.py new file mode 100644 index 0000000..c9448d1 --- /dev/null +++ b/model_utils/tests/test_models/test_model_tracker.py @@ -0,0 +1,150 @@ +from __future__ import unicode_literals + +import django + +from model_utils.tests.models import ( + ModelTracked, ModelTrackedFK, ModelTrackedNotDefault, ModelTrackedMultiple, InheritedModelTracked, +) + +from model_utils.tests.test_fields.test_field_tracker import ( + FieldTrackerTests, FieldTrackedModelCustomTests, + FieldTrackedModelMultiTests, FieldTrackerForeignKeyTests +) + + +class ModelTrackerTests(FieldTrackerTests): + + tracked_class = ModelTracked + + def test_pre_save_changed(self): + self.assertChanged() + self.instance.name = 'new age' + self.assertChanged() + self.instance.number = 8 + self.assertChanged() + self.instance.name = '' + self.assertChanged() + self.instance.mutable = [1,2,3] + self.assertChanged() + + def test_first_save(self): + self.assertHasChanged(name=True, number=True, mutable=True) + self.assertPrevious(name=None, number=None, mutable=None) + self.assertCurrent(name='', number=None, id=None, mutable=None) + self.assertChanged() + self.instance.name = 'retro' + self.instance.number = 4 + self.instance.mutable = [1,2,3] + self.assertHasChanged(name=True, number=True, mutable=True) + self.assertPrevious(name=None, number=None, mutable=None) + self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) + self.assertChanged() + # Django 1.4 doesn't have update_fields + if django.VERSION >= (1, 5, 0): + self.instance.save(update_fields=[]) + self.assertHasChanged(name=True, number=True, mutable=True) + self.assertPrevious(name=None, number=None, mutable=None) + self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) + self.assertChanged() + with self.assertRaises(ValueError): + self.instance.save(update_fields=['number']) + + def test_pre_save_has_changed(self): + self.assertHasChanged(name=True, number=True) + self.instance.name = 'new age' + self.assertHasChanged(name=True, number=True) + self.instance.number = 7 + self.assertHasChanged(name=True, number=True) + + +class ModelTrackedModelCustomTests(FieldTrackedModelCustomTests): + + tracked_class = ModelTrackedNotDefault + + def test_first_save(self): + self.assertHasChanged(name=True, number=True) + self.assertPrevious(name=None, number=None) + self.assertCurrent(name='') + self.assertChanged() + self.instance.name = 'retro' + self.instance.number = 4 + self.assertHasChanged(name=True, number=True) + self.assertPrevious(name=None, number=None) + self.assertCurrent(name='retro') + self.assertChanged() + + def test_pre_save_has_changed(self): + self.assertHasChanged(name=True, number=True) + self.instance.name = 'new age' + self.assertHasChanged(name=True, number=True) + self.instance.number = 7 + self.assertHasChanged(name=True, number=True) + + def test_pre_save_changed(self): + self.assertChanged() + self.instance.name = 'new age' + self.assertChanged() + self.instance.number = 8 + self.assertChanged() + self.instance.name = '' + self.assertChanged() + + +class ModelTrackedModelMultiTests(FieldTrackedModelMultiTests): + + tracked_class = ModelTrackedMultiple + + def test_pre_save_has_changed(self): + self.tracker = self.instance.name_tracker + self.assertHasChanged(name=True, number=True) + self.instance.name = 'new age' + self.assertHasChanged(name=True, number=True) + self.tracker = self.instance.number_tracker + self.assertHasChanged(name=True, number=True) + self.instance.name = 'new age' + self.assertHasChanged(name=True, number=True) + + def test_pre_save_changed(self): + self.tracker = self.instance.name_tracker + self.assertChanged() + self.instance.name = 'new age' + self.assertChanged() + self.instance.number = 8 + self.assertChanged() + self.instance.name = '' + self.assertChanged() + self.tracker = self.instance.number_tracker + self.assertChanged() + self.instance.name = 'new age' + self.assertChanged() + self.instance.number = 8 + self.assertChanged() + + +class ModelTrackerForeignKeyTests(FieldTrackerForeignKeyTests): + + fk_class = ModelTracked + tracked_class = ModelTrackedFK + + def test_custom_without_id(self): + with self.assertNumQueries(2): + self.tracked_class.objects.get() + self.tracker = self.instance.custom_tracker_without_id + self.assertChanged() + self.assertPrevious() + self.assertCurrent(fk=self.old_fk) + self.instance.fk = self.fk_class.objects.create(number=8) + self.assertNotEqual(self.instance.fk, self.old_fk) + self.assertChanged(fk=self.old_fk) + self.assertPrevious(fk=self.old_fk) + self.assertCurrent(fk=self.instance.fk) + + +class InheritedModelTrackerTests(ModelTrackerTests): + + tracked_class = InheritedModelTracked + + def test_child_fields_not_tracked(self): + self.name2 = 'test' + self.assertEqual(self.tracker.previous('name2'), None) + self.assertTrue(self.tracker.has_changed('name2')) diff --git a/model_utils/tests/test_models/test_softdeletable_model.py b/model_utils/tests/test_models/test_softdeletable_model.py new file mode 100644 index 0000000..dc5e629 --- /dev/null +++ b/model_utils/tests/test_models/test_softdeletable_model.py @@ -0,0 +1,38 @@ +from __future__ import unicode_literals + +from django.db.utils import ConnectionDoesNotExist +from django.test import TestCase + +from model_utils.tests.models import SoftDeletable + + +class SoftDeletableModelTests(TestCase): + def test_can_only_see_not_removed_entries(self): + SoftDeletable.objects.create(name='a', is_removed=True) + SoftDeletable.objects.create(name='b', is_removed=False) + + queryset = SoftDeletable.objects.all() + + self.assertEqual(queryset.count(), 1) + self.assertEqual(queryset[0].name, 'b') + + def test_instance_cannot_be_fully_deleted(self): + instance = SoftDeletable.objects.create(name='a') + + instance.delete() + + self.assertEqual(SoftDeletable.objects.count(), 0) + self.assertEqual(SoftDeletable.all_objects.count(), 1) + + def test_instance_cannot_be_fully_deleted_via_queryset(self): + SoftDeletable.objects.create(name='a') + + SoftDeletable.objects.all().delete() + + self.assertEqual(SoftDeletable.objects.count(), 0) + self.assertEqual(SoftDeletable.all_objects.count(), 1) + + def test_delete_instance_no_connection(self): + obj = SoftDeletable.objects.create(name='a') + + self.assertRaises(ConnectionDoesNotExist, obj.delete, using='other') diff --git a/model_utils/tests/test_models/test_status_model.py b/model_utils/tests/test_models/test_status_model.py new file mode 100644 index 0000000..995b4c6 --- /dev/null +++ b/model_utils/tests/test_models/test_status_model.py @@ -0,0 +1,46 @@ +from datetime import datetime + +from freezegun import freeze_time + +from django.test.testcases import TestCase + +from model_utils.tests.models import Status, StatusPlainTuple + + +class StatusModelTests(TestCase): + def setUp(self): + self.model = Status + self.on_hold = Status.STATUS.on_hold + self.active = Status.STATUS.active + + def test_created(self): + with freeze_time(datetime(2016, 1, 1)): + c1 = self.model.objects.create() + self.assertTrue(c1.status_changed, datetime(2016, 1, 1)) + + c2 = self.model.objects.create() + self.assertEqual(self.model.active.count(), 2) + self.assertEqual(self.model.deleted.count(), 0) + + def test_modification(self): + t1 = self.model.objects.create() + date_created = t1.status_changed + t1.status = self.on_hold + t1.save() + self.assertEqual(self.model.active.count(), 0) + self.assertEqual(self.model.on_hold.count(), 1) + self.assertTrue(t1.status_changed > date_created) + date_changed = t1.status_changed + t1.save() + self.assertEqual(t1.status_changed, date_changed) + date_active_again = t1.status_changed + t1.status = self.active + t1.save() + self.assertTrue(t1.status_changed > date_active_again) + + +class StatusModelPlainTupleTests(StatusModelTests): + def setUp(self): + self.model = StatusPlainTuple + self.on_hold = StatusPlainTuple.STATUS[2][0] + self.active = StatusPlainTuple.STATUS[0][0] diff --git a/model_utils/tests/test_models/test_timeframed_model.py b/model_utils/tests/test_models/test_timeframed_model.py new file mode 100644 index 0000000..993a339 --- /dev/null +++ b/model_utils/tests/test_models/test_timeframed_model.py @@ -0,0 +1,47 @@ +from __future__ import unicode_literals + +from datetime import datetime, timedelta + +from django.db import models +from django.core.exceptions import ImproperlyConfigured +from django.test import TestCase + +from model_utils.managers import QueryManager +from model_utils.models import TimeFramedModel +from model_utils.tests.models import TimeFrame, TimeFrameManagerAdded + + +class TimeFramedModelTests(TestCase): + def setUp(self): + self.now = datetime.now() + + def test_not_yet_begun(self): + TimeFrame.objects.create(start=self.now + timedelta(days=2)) + self.assertEqual(TimeFrame.timeframed.count(), 0) + + def test_finished(self): + TimeFrame.objects.create(end=self.now - timedelta(days=1)) + self.assertEqual(TimeFrame.timeframed.count(), 0) + + def test_no_end(self): + TimeFrame.objects.create(start=self.now - timedelta(days=10)) + self.assertEqual(TimeFrame.timeframed.count(), 1) + + def test_no_start(self): + TimeFrame.objects.create(end=self.now + timedelta(days=2)) + self.assertEqual(TimeFrame.timeframed.count(), 1) + + def test_within_range(self): + TimeFrame.objects.create(start=self.now - timedelta(days=1), + end=self.now + timedelta(days=1)) + self.assertEqual(TimeFrame.timeframed.count(), 1) + + +class TimeFrameManagerAddedTests(TestCase): + def test_manager_available(self): + self.assertTrue(isinstance(TimeFrameManagerAdded.timeframed, QueryManager)) + + def test_conflict_error(self): + with self.assertRaises(ImproperlyConfigured): + class ErrorModel(TimeFramedModel): + timeframed = models.BooleanField() diff --git a/model_utils/tests/test_models/test_timestamped_model.py b/model_utils/tests/test_models/test_timestamped_model.py new file mode 100644 index 0000000..221d682 --- /dev/null +++ b/model_utils/tests/test_models/test_timestamped_model.py @@ -0,0 +1,25 @@ +from __future__ import unicode_literals + +from datetime import datetime + +from freezegun import freeze_time + +from django.test import TestCase + +from model_utils.tests.models import TimeStamp + + +class TimeStampedModelTests(TestCase): + def test_created(self): + with freeze_time(datetime(2016, 1, 1)): + t1 = TimeStamp.objects.create() + self.assertEqual(t1.created, datetime(2016, 1, 1)) + + def test_modified(self): + with freeze_time(datetime(2016, 1, 1)): + t1 = TimeStamp.objects.create() + + with freeze_time(datetime(2016, 1, 2)): + t1.save() + + self.assertEqual(t1.modified, datetime(2016, 1, 2)) diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index fafef32..39eced3 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -1,2047 +1,6 @@ -from __future__ import unicode_literals - -from datetime import datetime, timedelta - -from freezegun import freeze_time - -try: - from unittest import skipUnless -except ImportError: # Python 2.6 - from django.utils.unittest import skipUnless - -from freezegun import freeze_time - -import django -from django.db import models -from django.db.models.fields import FieldDoesNotExist -from django.db.utils import ConnectionDoesNotExist -from django.utils.six import text_type -from django.core.exceptions import ImproperlyConfigured, FieldError -from django.core.management import call_command -from django.test import TestCase - -from model_utils import Choices, FieldTracker -from model_utils.fields import get_excerpt, MonitorField, StatusField -from model_utils.managers import QueryManager -from model_utils.models import StatusModel, TimeFramedModel -from model_utils.tests.models import ( - InheritanceManagerTestRelated, InheritanceManagerTestGrandChild1, - InheritanceManagerTestGrandChild1_2, - InheritanceManagerTestParent, InheritanceManagerTestChild1, - InheritanceManagerTestChild2, TimeStamp, Post, Article, Status, - StatusPlainTuple, TimeFrame, Monitored, MonitorWhen, MonitorWhenEmpty, StatusManagerAdded, - TimeFrameManagerAdded, SplitFieldAbstractParent, - ModelTracked, ModelTrackedFK, ModelTrackedNotDefault, ModelTrackedMultiple, InheritedModelTracked, - Tracked, TrackedFK, InheritedTrackedFK, TrackedNotDefault, TrackedNonFieldAttr, TrackedMultiple, - InheritedTracked, TrackedFileField, StatusFieldDefaultFilled, StatusFieldDefaultNotFilled, - InheritanceManagerTestChild3, StatusFieldChoicesName, - SoftDeletable, DoubleMonitored) - - -class MigrationsTests(TestCase): - @skipUnless(django.VERSION >= (1, 7, 0), "test only applies to Django 1.7+") - def test_makemigrations(self): - call_command('makemigrations', dry_run=True) - - -class GetExcerptTests(TestCase): - def test_split(self): - e = get_excerpt("some content\n\n\n\nsome more") - self.assertEqual(e, 'some content\n') - - - def test_auto_split(self): - e = get_excerpt("para one\n\npara two\n\npara three") - self.assertEqual(e, 'para one\n\npara two') - - - def test_middle_of_para(self): - e = get_excerpt("some text\n\nmore text") - self.assertEqual(e, 'some text') - - - def test_middle_of_line(self): - e = get_excerpt("some text more text") - self.assertEqual(e, "some text more text") - - - -class SplitFieldTests(TestCase): - full_text = 'summary\n\n\n\nmore' - excerpt = 'summary\n' - - - def setUp(self): - self.post = Article.objects.create( - title='example post', body=self.full_text) - - - def test_unicode_content(self): - self.assertEqual(text_type(self.post.body), self.full_text) - - - def test_excerpt(self): - self.assertEqual(self.post.body.excerpt, self.excerpt) - - - def test_content(self): - self.assertEqual(self.post.body.content, self.full_text) - - - def test_has_more(self): - self.assertTrue(self.post.body.has_more) - - - def test_not_has_more(self): - post = Article.objects.create(title='example 2', - body='some text\n\nsome more\n') - self.assertFalse(post.body.has_more) - - - def test_load_back(self): - post = Article.objects.get(pk=self.post.pk) - self.assertEqual(post.body.content, self.post.body.content) - self.assertEqual(post.body.excerpt, self.post.body.excerpt) - - - def test_assign_to_body(self): - new_text = 'different\n\n\n\nother' - self.post.body = new_text - self.post.save() - self.assertEqual(text_type(self.post.body), new_text) - - - def test_assign_to_content(self): - new_text = 'different\n\n\n\nother' - self.post.body.content = new_text - self.post.save() - self.assertEqual(text_type(self.post.body), new_text) - - - def test_assign_to_excerpt(self): - with self.assertRaises(AttributeError): - self.post.body.excerpt = 'this should fail' - - - def test_access_via_class(self): - with self.assertRaises(AttributeError): - Article.body - - - def test_none(self): - a = Article(title='Some Title', body=None) - self.assertEqual(a.body, None) - - - def test_assign_splittext(self): - a = Article(title='Some Title') - a.body = self.post.body - self.assertEqual(a.body.excerpt, 'summary\n') - - - def test_value_to_string(self): - f = self.post._meta.get_field('body') - self.assertEqual(f.value_to_string(self.post), self.full_text) - - - def test_abstract_inheritance(self): - class Child(SplitFieldAbstractParent): - pass - - self.assertEqual( - [f.name for f in Child._meta.fields], - ["id", "content", "_content_excerpt"]) - - - -class MonitorFieldTests(TestCase): - def setUp(self): - with freeze_time(datetime(2016, 1, 1, 10, 0, 0)): - self.instance = Monitored(name='Charlie') - self.created = self.instance.name_changed - - - def test_save_no_change(self): - self.instance.save() - self.assertEqual(self.instance.name_changed, self.created) - - - def test_save_changed(self): - with freeze_time(datetime(2016, 1, 1, 12, 0, 0)): - self.instance.name = 'Maria' - self.instance.save() - self.assertEqual(self.instance.name_changed, datetime(2016, 1, 1, 12, 0, 0)) - - - def test_double_save(self): - self.instance.name = 'Jose' - self.instance.save() - changed = self.instance.name_changed - self.instance.save() - self.assertEqual(self.instance.name_changed, changed) - - - def test_no_monitor_arg(self): - with self.assertRaises(TypeError): - MonitorField() - - - -class MonitorWhenFieldTests(TestCase): - """ - Will record changes only when name is 'Jose' or 'Maria' - """ - def setUp(self): - with freeze_time(datetime(2016, 1, 1, 10, 0, 0)): - self.instance = MonitorWhen(name='Charlie') - self.created = self.instance.name_changed - - - def test_save_no_change(self): - self.instance.save() - self.assertEqual(self.instance.name_changed, self.created) - - - def test_save_changed_to_Jose(self): - with freeze_time(datetime(2016, 1, 1, 12, 0, 0)): - self.instance.name = 'Jose' - self.instance.save() - self.assertEqual(self.instance.name_changed, datetime(2016, 1, 1, 12, 0, 0)) - - - def test_save_changed_to_Maria(self): - with freeze_time(datetime(2016, 1, 1, 12, 0, 0)): - self.instance.name = 'Maria' - self.instance.save() - self.assertEqual(self.instance.name_changed, datetime(2016, 1, 1, 12, 0, 0)) - - - def test_save_changed_to_Pedro(self): - self.instance.name = 'Pedro' - self.instance.save() - self.assertEqual(self.instance.name_changed, self.created) - - - def test_double_save(self): - self.instance.name = 'Jose' - self.instance.save() - changed = self.instance.name_changed - self.instance.save() - self.assertEqual(self.instance.name_changed, changed) - - - -class MonitorWhenEmptyFieldTests(TestCase): - """ - Monitor should never be updated id when is an empty list. - """ - def setUp(self): - self.instance = MonitorWhenEmpty(name='Charlie') - self.created = self.instance.name_changed - - - def test_save_no_change(self): - self.instance.save() - self.assertEqual(self.instance.name_changed, self.created) - - - def test_save_changed_to_Jose(self): - self.instance.name = 'Jose' - self.instance.save() - self.assertEqual(self.instance.name_changed, self.created) - - - def test_save_changed_to_Maria(self): - self.instance.name = 'Maria' - self.instance.save() - self.assertEqual(self.instance.name_changed, self.created) - - -class MonitorDoubleFieldTests(TestCase): - - def setUp(self): - DoubleMonitored.objects.create(name='Charlie', name2='Charlie2') - - def test_recursion_error_with_only(self): - # Any field passed to only() is generating a recursion error - list(DoubleMonitored.objects.only('id')) - - def test_recursion_error_with_defer(self): - # Only monitored fields passed to defer() are failing - list(DoubleMonitored.objects.defer('name')) - - def test_monitor_still_works_with_deferred_fields_filtered_out_of_save_initial(self): - obj = DoubleMonitored.objects.defer('name').get(name='Charlie') - with freeze_time("2016-12-01"): - obj.name = 'Charlie2' - obj.save() - self.assertEqual(obj.name_changed, datetime(2016, 12, 1)) - - -class StatusFieldTests(TestCase): - - def test_status_with_default_filled(self): - instance = StatusFieldDefaultFilled() - self.assertEqual(instance.status, instance.STATUS.yes) - - def test_status_with_default_not_filled(self): - instance = StatusFieldDefaultNotFilled() - self.assertEqual(instance.status, instance.STATUS.no) - - def test_no_check_for_status(self): - field = StatusField(no_check_for_status=True) - # this model has no STATUS attribute, so checking for it would error - field.prepare_class(Article) - - def test_get_status_display(self): - instance = StatusFieldDefaultFilled() - self.assertEqual(instance.get_status_display(), "Yes") - - def test_choices_name(self): - StatusFieldChoicesName() - - -class ChoicesTests(TestCase): - def setUp(self): - self.STATUS = Choices('DRAFT', 'PUBLISHED') - - - def test_getattr(self): - self.assertEqual(self.STATUS.DRAFT, 'DRAFT') - - - def test_indexing(self): - self.assertEqual(self.STATUS['PUBLISHED'], 'PUBLISHED') - - - def test_iteration(self): - self.assertEqual(tuple(self.STATUS), (('DRAFT', 'DRAFT'), ('PUBLISHED', 'PUBLISHED'))) - - - def test_len(self): - self.assertEqual(len(self.STATUS), 2) - - - def test_repr(self): - self.assertEqual(repr(self.STATUS), "Choices" + repr(( - ('DRAFT', 'DRAFT', 'DRAFT'), - ('PUBLISHED', 'PUBLISHED', 'PUBLISHED'), - ))) - - - def test_wrong_length_tuple(self): - with self.assertRaises(ValueError): - Choices(('a',)) - - - def test_contains_value(self): - self.assertTrue('PUBLISHED' in self.STATUS) - self.assertTrue('DRAFT' in self.STATUS) - - - def test_doesnt_contain_value(self): - self.assertFalse('UNPUBLISHED' in self.STATUS) - - def test_deepcopy(self): - import copy - self.assertEqual(list(self.STATUS), - list(copy.deepcopy(self.STATUS))) - - - def test_equality(self): - self.assertEqual(self.STATUS, Choices('DRAFT', 'PUBLISHED')) - - - def test_inequality(self): - self.assertNotEqual(self.STATUS, ['DRAFT', 'PUBLISHED']) - self.assertNotEqual(self.STATUS, Choices('DRAFT')) - - - def test_composability(self): - self.assertEqual(Choices('DRAFT') + Choices('PUBLISHED'), self.STATUS) - self.assertEqual(Choices('DRAFT') + ('PUBLISHED',), self.STATUS) - self.assertEqual(('DRAFT',) + Choices('PUBLISHED'), self.STATUS) - - - def test_option_groups(self): - c = Choices(('group a', ['one', 'two']), ['group b', ('three',)]) - self.assertEqual( - list(c), - [ - ('group a', [('one', 'one'), ('two', 'two')]), - ('group b', [('three', 'three')]), - ], - ) - - -class LabelChoicesTests(ChoicesTests): - def setUp(self): - self.STATUS = Choices( - ('DRAFT', 'is draft'), - ('PUBLISHED', 'is published'), - 'DELETED', - ) - - - def test_iteration(self): - self.assertEqual(tuple(self.STATUS), ( - ('DRAFT', 'is draft'), - ('PUBLISHED', 'is published'), - ('DELETED', 'DELETED')) - ) - - - def test_indexing(self): - self.assertEqual(self.STATUS['PUBLISHED'], 'is published') - - - def test_default(self): - self.assertEqual(self.STATUS.DELETED, 'DELETED') - - - def test_provided(self): - self.assertEqual(self.STATUS.DRAFT, 'DRAFT') - - - def test_len(self): - self.assertEqual(len(self.STATUS), 3) - - - def test_equality(self): - self.assertEqual(self.STATUS, Choices( - ('DRAFT', 'is draft'), - ('PUBLISHED', 'is published'), - 'DELETED', - )) - - - def test_inequality(self): - self.assertNotEqual(self.STATUS, [ - ('DRAFT', 'is draft'), - ('PUBLISHED', 'is published'), - 'DELETED' - ]) - self.assertNotEqual(self.STATUS, Choices('DRAFT')) - - - def test_repr(self): - self.assertEqual(repr(self.STATUS), "Choices" + repr(( - ('DRAFT', 'DRAFT', 'is draft'), - ('PUBLISHED', 'PUBLISHED', 'is published'), - ('DELETED', 'DELETED', 'DELETED'), - ))) - - - def test_contains_value(self): - self.assertTrue('PUBLISHED' in self.STATUS) - self.assertTrue('DRAFT' in self.STATUS) - # This should be True, because both the display value - # and the internal representation are both DELETED. - self.assertTrue('DELETED' in self.STATUS) - - - def test_doesnt_contain_value(self): - self.assertFalse('UNPUBLISHED' in self.STATUS) - - - def test_doesnt_contain_display_value(self): - self.assertFalse('is draft' in self.STATUS) - - - def test_composability(self): - self.assertEqual( - Choices(('DRAFT', 'is draft',)) + Choices(('PUBLISHED', 'is published'), 'DELETED'), - self.STATUS - ) - - self.assertEqual( - (('DRAFT', 'is draft',),) + Choices(('PUBLISHED', 'is published'), 'DELETED'), - self.STATUS - ) - - self.assertEqual( - Choices(('DRAFT', 'is draft',)) + (('PUBLISHED', 'is published'), 'DELETED'), - self.STATUS - ) - - - def test_option_groups(self): - c = Choices( - ('group a', [(1, 'one'), (2, 'two')]), - ['group b', ((3, 'three'),)] - ) - self.assertEqual( - list(c), - [ - ('group a', [(1, 'one'), (2, 'two')]), - ('group b', [(3, 'three')]), - ], - ) - - - -class IdentifierChoicesTests(ChoicesTests): - def setUp(self): - self.STATUS = Choices( - (0, 'DRAFT', 'is draft'), - (1, 'PUBLISHED', 'is published'), - (2, 'DELETED', 'is deleted')) - - - def test_iteration(self): - self.assertEqual(tuple(self.STATUS), ( - (0, 'is draft'), - (1, 'is published'), - (2, 'is deleted'))) - - - def test_indexing(self): - self.assertEqual(self.STATUS[1], 'is published') - - - def test_getattr(self): - self.assertEqual(self.STATUS.DRAFT, 0) - - - def test_len(self): - self.assertEqual(len(self.STATUS), 3) - - - def test_repr(self): - self.assertEqual(repr(self.STATUS), "Choices" + repr(( - (0, 'DRAFT', 'is draft'), - (1, 'PUBLISHED', 'is published'), - (2, 'DELETED', 'is deleted'), - ))) - - - def test_contains_value(self): - self.assertTrue(0 in self.STATUS) - self.assertTrue(1 in self.STATUS) - self.assertTrue(2 in self.STATUS) - - - def test_doesnt_contain_value(self): - self.assertFalse(3 in self.STATUS) - - - def test_doesnt_contain_display_value(self): - self.assertFalse('is draft' in self.STATUS) - - - def test_doesnt_contain_python_attr(self): - self.assertFalse('PUBLISHED' in self.STATUS) - - - def test_equality(self): - self.assertEqual(self.STATUS, Choices( - (0, 'DRAFT', 'is draft'), - (1, 'PUBLISHED', 'is published'), - (2, 'DELETED', 'is deleted') - )) - - - def test_inequality(self): - self.assertNotEqual(self.STATUS, [ - (0, 'DRAFT', 'is draft'), - (1, 'PUBLISHED', 'is published'), - (2, 'DELETED', 'is deleted') - ]) - self.assertNotEqual(self.STATUS, Choices('DRAFT')) - - - def test_composability(self): - self.assertEqual( - Choices( - (0, 'DRAFT', 'is draft'), - (1, 'PUBLISHED', 'is published') - ) + Choices( - (2, 'DELETED', 'is deleted'), - ), - self.STATUS - ) - - self.assertEqual( - Choices( - (0, 'DRAFT', 'is draft'), - (1, 'PUBLISHED', 'is published') - ) + ( - (2, 'DELETED', 'is deleted'), - ), - self.STATUS - ) - - self.assertEqual( - ( - (0, 'DRAFT', 'is draft'), - (1, 'PUBLISHED', 'is published') - ) + Choices( - (2, 'DELETED', 'is deleted'), - ), - self.STATUS - ) - - - def test_option_groups(self): - c = Choices( - ('group a', [(1, 'ONE', 'one'), (2, 'TWO', 'two')]), - ['group b', ((3, 'THREE', 'three'),)] - ) - self.assertEqual( - list(c), - [ - ('group a', [(1, 'one'), (2, 'two')]), - ('group b', [(3, 'three')]), - ], - ) - - -class InheritanceManagerTests(TestCase): - def setUp(self): - self.child1 = InheritanceManagerTestChild1.objects.create() - self.child2 = InheritanceManagerTestChild2.objects.create() - self.grandchild1 = InheritanceManagerTestGrandChild1.objects.create() - self.grandchild1_2 = \ - InheritanceManagerTestGrandChild1_2.objects.create() - - - def get_manager(self): - return InheritanceManagerTestParent.objects - - - def test_normal(self): - children = set([ - InheritanceManagerTestParent(pk=self.child1.pk), - InheritanceManagerTestParent(pk=self.child2.pk), - InheritanceManagerTestParent(pk=self.grandchild1.pk), - InheritanceManagerTestParent(pk=self.grandchild1_2.pk), - ]) - self.assertEqual(set(self.get_manager().all()), children) - - - def test_select_all_subclasses(self): - children = set([self.child1, self.child2]) - if django.VERSION >= (1, 6, 0): - children.add(self.grandchild1) - children.add(self.grandchild1_2) - else: - children.add(InheritanceManagerTestChild1(pk=self.grandchild1.pk)) - children.add(InheritanceManagerTestChild1(pk=self.grandchild1_2.pk)) - self.assertEqual( - set(self.get_manager().select_subclasses()), children) - - - def test_select_subclasses_invalid_relation(self): - """ - If an invalid relation string is provided, we can provide the user - with a list which is valid, rather than just have the select_related() - raise an AttributeError further in. - """ - regex = '^.+? is not in the discovered subclasses, tried:.+$' - with self.assertRaisesRegexp(ValueError, regex): - self.get_manager().select_subclasses('user') - - - def test_select_specific_subclasses(self): - children = set([ - self.child1, - InheritanceManagerTestParent(pk=self.child2.pk), - InheritanceManagerTestChild1(pk=self.grandchild1.pk), - InheritanceManagerTestChild1(pk=self.grandchild1_2.pk), - ]) - self.assertEqual( - set( - self.get_manager().select_subclasses( - "inheritancemanagertestchild1") - ), - children, - ) - - - @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") - def test_select_specific_grandchildren(self): - children = set([ - InheritanceManagerTestParent(pk=self.child1.pk), - InheritanceManagerTestParent(pk=self.child2.pk), - self.grandchild1, - InheritanceManagerTestParent(pk=self.grandchild1_2.pk), - ]) - self.assertEqual( - set( - self.get_manager().select_subclasses( - "inheritancemanagertestchild1__inheritancemanagertestgrandchild1" - ) - ), - children, - ) - - - @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") - def test_children_and_grandchildren(self): - children = set([ - self.child1, - InheritanceManagerTestParent(pk=self.child2.pk), - self.grandchild1, - InheritanceManagerTestChild1(pk=self.grandchild1_2.pk), - ]) - self.assertEqual( - set( - self.get_manager().select_subclasses( - "inheritancemanagertestchild1", - "inheritancemanagertestchild1__inheritancemanagertestgrandchild1" - ) - ), - children, - ) - - - def test_get_subclass(self): - self.assertEqual( - self.get_manager().get_subclass(pk=self.child1.pk), - self.child1) - - - def test_get_subclass_on_queryset(self): - self.assertEqual( - self.get_manager().all().get_subclass(pk=self.child1.pk), - self.child1) - - - def test_prior_select_related(self): - with self.assertNumQueries(1): - obj = self.get_manager().select_related( - "inheritancemanagertestchild1").select_subclasses( - "inheritancemanagertestchild2").get(pk=self.child1.pk) - obj.inheritancemanagertestchild1 - - - @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") - def test_version_determining_any_depth(self): - self.assertIsNone(self.get_manager().all()._get_maximum_depth()) - - - @skipUnless(django.VERSION < (1, 6, 0), "test only applies to Django < 1.6") - def test_version_determining_only_child_depth(self): - self.assertEqual(1, self.get_manager().all()._get_maximum_depth()) - - - @skipUnless(django.VERSION < (1, 6, 0), "test only applies to Django < 1.6") - def test_manually_specifying_parent_fk_only_children(self): - """ - given a Model which inherits from another Model, but also declares - the OneToOne link manually using `related_name` and `parent_link`, - ensure that the relation names and subclasses are obtained correctly. - """ - child3 = InheritanceManagerTestChild3.objects.create() - results = InheritanceManagerTestParent.objects.all().select_subclasses() - - expected_objs = [self.child1, self.child2, - InheritanceManagerTestChild1(pk=self.grandchild1.pk), - InheritanceManagerTestChild1(pk=self.grandchild1_2.pk), - child3] - self.assertEqual(list(results), expected_objs) - - expected_related_names = [ - 'inheritancemanagertestchild1', - 'inheritancemanagertestchild2', - 'manual_onetoone', # this was set via parent_link & related_name - ] - self.assertEqual(set(results.subclasses), - set(expected_related_names)) - - - @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") - def test_manually_specifying_parent_fk_including_grandchildren(self): - """ - given a Model which inherits from another Model, but also declares - the OneToOne link manually using `related_name` and `parent_link`, - ensure that the relation names and subclasses are obtained correctly. - """ - child3 = InheritanceManagerTestChild3.objects.create() - results = InheritanceManagerTestParent.objects.all().select_subclasses() - - expected_objs = [self.child1, self.child2, self.grandchild1, - self.grandchild1_2, child3] - self.assertEqual(list(results), expected_objs) - - expected_related_names = [ - 'inheritancemanagertestchild1__inheritancemanagertestgrandchild1', - 'inheritancemanagertestchild1__inheritancemanagertestgrandchild1_2', - 'inheritancemanagertestchild1', - 'inheritancemanagertestchild2', - 'manual_onetoone', # this was set via parent_link & related_name - ] - self.assertEqual(set(results.subclasses), - set(expected_related_names)) - - - def test_manually_specifying_parent_fk_single_subclass(self): - """ - Using a string related_name when the relation is manually defined - instead of implicit should still work in the same way. - """ - related_name = 'manual_onetoone' - child3 = InheritanceManagerTestChild3.objects.create() - results = InheritanceManagerTestParent.objects.all().select_subclasses(related_name) - - expected_objs = [InheritanceManagerTestParent(pk=self.child1.pk), - InheritanceManagerTestParent(pk=self.child2.pk), - InheritanceManagerTestParent(pk=self.grandchild1.pk), - InheritanceManagerTestParent(pk=self.grandchild1_2.pk), - child3] - self.assertEqual(list(results), expected_objs) - expected_related_names = [related_name] - self.assertEqual(set(results.subclasses), - set(expected_related_names)) - - - def test_filter_on_values_queryset(self): - queryset = InheritanceManagerTestChild1.objects.values('id').filter(pk=self.child1.pk) - self.assertEqual(list(queryset), [{'id': self.child1.pk}]) - - - @skipUnless(django.VERSION >= (1, 9, 0), "test only applies to Django 1.9+") - def test_dj19_values_list_on_select_subclasses(self): - """ - Using `select_subclasses` in conjunction with `values_list()` raised an - exception in `_get_sub_obj_recurse()` because the result of `values_list()` - is either a `tuple` or primitive objects if `flat=True` is specified, - because no type checking was done prior to fetching child nodes. - - Django versions below 1.9 are not affected by this bug. - """ - - # Querysets are cast to lists to force immediate evaluation. - # No exceptions must be thrown. - - # No argument to select_subclasses - objs_1 = list( - self.get_manager(). - select_subclasses(). - values_list('id') - ) - - # String argument to select_subclasses - objs_2 = list( - self.get_manager(). - select_subclasses( - "inheritancemanagertestchild2" - ). - values_list('id') - ) - - # String argument to select_subclasses - objs_3 = list( - self.get_manager(). - select_subclasses( - InheritanceManagerTestChild2 - ). - values_list('id') - ) - - assert all(( - isinstance(objs_1, list), - isinstance(objs_2, list), - isinstance(objs_3, list), - )) - - assert objs_1 == objs_2 == objs_3 - - -class InheritanceManagerUsingModelsTests(TestCase): - - def setUp(self): - self.parent1 = InheritanceManagerTestParent.objects.create() - self.child1 = InheritanceManagerTestChild1.objects.create() - self.child2 = InheritanceManagerTestChild2.objects.create() - self.grandchild1 = InheritanceManagerTestGrandChild1.objects.create() - self.grandchild1_2 = InheritanceManagerTestGrandChild1_2.objects.create() - - - def test_select_subclass_by_child_model(self): - """ - Confirm that passing a child model works the same as passing the - select_related manually - """ - objs = InheritanceManagerTestParent.objects.select_subclasses( - "inheritancemanagertestchild1").order_by('pk') - objsmodels = InheritanceManagerTestParent.objects.select_subclasses( - InheritanceManagerTestChild1).order_by('pk') - self.assertEqual(objs.subclasses, objsmodels.subclasses) - self.assertEqual(list(objs), list(objsmodels)) - - - @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") - def test_select_subclass_by_grandchild_model(self): - """ - Confirm that passing a grandchild model works the same as passing the - select_related manually - """ - objs = InheritanceManagerTestParent.objects.select_subclasses( - "inheritancemanagertestchild1__inheritancemanagertestgrandchild1")\ - .order_by('pk') - objsmodels = InheritanceManagerTestParent.objects.select_subclasses( - InheritanceManagerTestGrandChild1).order_by('pk') - self.assertEqual(objs.subclasses, objsmodels.subclasses) - self.assertEqual(list(objs), list(objsmodels)) - - - @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") - def test_selecting_all_subclasses_specifically_grandchildren(self): - """ - A bare select_subclasses() should achieve the same results as doing - select_subclasses and specifying all possible subclasses. - This test checks grandchildren, so only works on 1.6>= - """ - objs = InheritanceManagerTestParent.objects.select_subclasses().order_by('pk') - objsmodels = InheritanceManagerTestParent.objects.select_subclasses( - InheritanceManagerTestChild1, InheritanceManagerTestChild2, - InheritanceManagerTestChild3, - InheritanceManagerTestGrandChild1, - InheritanceManagerTestGrandChild1_2).order_by('pk') - self.assertEqual(set(objs.subclasses), set(objsmodels.subclasses)) - self.assertEqual(list(objs), list(objsmodels)) - - - def test_selecting_all_subclasses_specifically_children(self): - """ - A bare select_subclasses() should achieve the same results as doing - select_subclasses and specifying all possible subclasses. - - Note: This is sort of the same test as - `test_selecting_all_subclasses_specifically_grandchildren` but it - specifically switches what models are used because that happens - behind the scenes in a bare select_subclasses(), so we need to - emulate it. - """ - objs = InheritanceManagerTestParent.objects.select_subclasses().order_by('pk') - - if django.VERSION >= (1, 6, 0): - models = (InheritanceManagerTestChild1, - InheritanceManagerTestChild2, - InheritanceManagerTestChild3, - InheritanceManagerTestGrandChild1, - InheritanceManagerTestGrandChild1_2) - else: - models = (InheritanceManagerTestChild1, - InheritanceManagerTestChild2, - InheritanceManagerTestChild3) - - objsmodels = InheritanceManagerTestParent.objects.select_subclasses( - *models).order_by('pk') - # order shouldn't matter, I don't think, as long as the resulting - # queryset (when cast to a list) is the same. - self.assertEqual(set(objs.subclasses), set(objsmodels.subclasses)) - self.assertEqual(list(objs), list(objsmodels)) - - - def test_select_subclass_just_self(self): - """ - Passing in the same model as the manager/queryset is bound against - (ie: the root parent) should have no effect on the result set. - """ - objsmodels = InheritanceManagerTestParent.objects.select_subclasses( - InheritanceManagerTestParent).order_by('pk') - self.assertEqual([], objsmodels.subclasses) - self.assertEqual(list(objsmodels), [ - InheritanceManagerTestParent(pk=self.parent1.pk), - InheritanceManagerTestParent(pk=self.child1.pk), - InheritanceManagerTestParent(pk=self.child2.pk), - InheritanceManagerTestParent(pk=self.grandchild1.pk), - InheritanceManagerTestParent(pk=self.grandchild1_2.pk), - ]) - - - def test_select_subclass_invalid_related_model(self): - """ - Confirming that giving a stupid model doesn't work. - """ - regex = '^.+? is not a subclass of .+$' - with self.assertRaisesRegexp(ValueError, regex): - InheritanceManagerTestParent.objects.select_subclasses( - TimeFrame).order_by('pk') - - - - @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") - def test_mixing_strings_and_classes_with_grandchildren(self): - """ - Given arguments consisting of both strings and model classes, - ensure the right resolutions take place, accounting for the extra - depth (grandchildren etc) 1.6> allows. - """ - objs = InheritanceManagerTestParent.objects.select_subclasses( - "inheritancemanagertestchild2", - InheritanceManagerTestGrandChild1_2).order_by('pk') - expecting = ['inheritancemanagertestchild1__inheritancemanagertestgrandchild1_2', - 'inheritancemanagertestchild2'] - self.assertEqual(set(objs.subclasses), set(expecting)) - expecting2 = [ - InheritanceManagerTestParent(pk=self.parent1.pk), - InheritanceManagerTestParent(pk=self.child1.pk), - InheritanceManagerTestChild2(pk=self.child2.pk), - InheritanceManagerTestParent(pk=self.grandchild1.pk), - InheritanceManagerTestGrandChild1_2(pk=self.grandchild1_2.pk), - ] - self.assertEqual(list(objs), expecting2) - - - def test_mixing_strings_and_classes_with_children(self): - """ - Given arguments consisting of both strings and model classes, - ensure the right resolutions take place, walking down as far as - children. - """ - objs = InheritanceManagerTestParent.objects.select_subclasses( - "inheritancemanagertestchild2", - InheritanceManagerTestChild1).order_by('pk') - expecting = ['inheritancemanagertestchild1', - 'inheritancemanagertestchild2'] - - self.assertEqual(set(objs.subclasses), set(expecting)) - expecting2 = [ - InheritanceManagerTestParent(pk=self.parent1.pk), - InheritanceManagerTestChild1(pk=self.child1.pk), - InheritanceManagerTestChild2(pk=self.child2.pk), - InheritanceManagerTestChild1(pk=self.grandchild1.pk), - InheritanceManagerTestChild1(pk=self.grandchild1_2.pk), - ] - self.assertEqual(list(objs), expecting2) - - - def test_duplications(self): - """ - Check that even if the same thing is provided as a string and a model - that the right results are retrieved. - """ - # mixing strings and models which evaluate to the same thing is fine. - objs = InheritanceManagerTestParent.objects.select_subclasses( - "inheritancemanagertestchild2", - InheritanceManagerTestChild2).order_by('pk') - self.assertEqual(list(objs), [ - InheritanceManagerTestParent(pk=self.parent1.pk), - InheritanceManagerTestParent(pk=self.child1.pk), - InheritanceManagerTestChild2(pk=self.child2.pk), - InheritanceManagerTestParent(pk=self.grandchild1.pk), - InheritanceManagerTestParent(pk=self.grandchild1_2.pk), - ]) - - - @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") - def test_child_doesnt_accidentally_get_parent(self): - """ - Given a Child model which also has an InheritanceManager, - none of the returned objects should be Parent objects. - """ - objs = InheritanceManagerTestChild1.objects.select_subclasses( - InheritanceManagerTestGrandChild1).order_by('pk') - self.assertEqual([ - InheritanceManagerTestChild1(pk=self.child1.pk), - InheritanceManagerTestGrandChild1(pk=self.grandchild1.pk), - InheritanceManagerTestChild1(pk=self.grandchild1_2.pk), - ], list(objs)) - - - def test_manually_specifying_parent_fk_only_specific_child(self): - """ - given a Model which inherits from another Model, but also declares - the OneToOne link manually using `related_name` and `parent_link`, - ensure that the relation names and subclasses are obtained correctly. - """ - child3 = InheritanceManagerTestChild3.objects.create() - results = InheritanceManagerTestParent.objects.all().select_subclasses( - InheritanceManagerTestChild3) - - expected_objs = [InheritanceManagerTestParent(pk=self.parent1.pk), - InheritanceManagerTestParent(pk=self.child1.pk), - InheritanceManagerTestParent(pk=self.child2.pk), - InheritanceManagerTestParent(pk=self.grandchild1.pk), - InheritanceManagerTestParent(pk=self.grandchild1_2.pk), - child3] - self.assertEqual(list(results), expected_objs) - - expected_related_names = ['manual_onetoone'] - self.assertEqual(set(results.subclasses), - set(expected_related_names)) - - def test_extras_descend(self): - """ - Ensure that extra(select=) values are copied onto sub-classes. - """ - results = InheritanceManagerTestParent.objects.select_subclasses().extra( - select={'foo': 'id + 1'} - ) - self.assertTrue(all(result.foo == (result.id + 1) for result in results)) - - -class InheritanceManagerRelatedTests(InheritanceManagerTests): - def setUp(self): - self.related = InheritanceManagerTestRelated.objects.create() - self.child1 = InheritanceManagerTestChild1.objects.create( - related=self.related) - self.child2 = InheritanceManagerTestChild2.objects.create( - related=self.related) - self.grandchild1 = InheritanceManagerTestGrandChild1.objects.create(related=self.related) - self.grandchild1_2 = InheritanceManagerTestGrandChild1_2.objects.create(related=self.related) - - - def get_manager(self): - return self.related.imtests - - - def test_get_method_with_select_subclasses(self): - self.assertEqual( - InheritanceManagerTestParent.objects.select_subclasses().get( - id=self.child1.id), - self.child1) - - - def test_annotate_with_select_subclasses(self): - qs = InheritanceManagerTestParent.objects.select_subclasses().annotate( - models.Count('id')) - self.assertEqual(qs.get(id=self.child1.id).id__count, 1) - - - def test_annotate_with_named_arguments_with_select_subclasses(self): - qs = InheritanceManagerTestParent.objects.select_subclasses().annotate( - test_count=models.Count('id')) - self.assertEqual(qs.get(id=self.child1.id).test_count, 1) - - - def test_annotate_before_select_subclasses(self): - qs = InheritanceManagerTestParent.objects.annotate( - models.Count('id')).select_subclasses() - self.assertEqual(qs.get(id=self.child1.id).id__count, 1) - - - def test_annotate_with_named_arguments_before_select_subclasses(self): - qs = InheritanceManagerTestParent.objects.annotate( - test_count=models.Count('id')).select_subclasses() - self.assertEqual(qs.get(id=self.child1.id).test_count, 1) - - - -class TimeStampedModelTests(TestCase): - def test_created(self): - with freeze_time(datetime(2016, 1, 1)): - t1 = TimeStamp.objects.create() - self.assertEqual(t1.created, datetime(2016, 1, 1)) - - - def test_modified(self): - with freeze_time(datetime(2016, 1, 1)): - t1 = TimeStamp.objects.create() - - with freeze_time(datetime(2016, 1, 2)): - t1.save() - - self.assertEqual(t1.modified, datetime(2016, 1, 2)) - - - -class TimeFramedModelTests(TestCase): - def setUp(self): - self.now = datetime.now() - - - def test_not_yet_begun(self): - TimeFrame.objects.create(start=self.now+timedelta(days=2)) - self.assertEqual(TimeFrame.timeframed.count(), 0) - - - def test_finished(self): - TimeFrame.objects.create(end=self.now-timedelta(days=1)) - self.assertEqual(TimeFrame.timeframed.count(), 0) - - - def test_no_end(self): - TimeFrame.objects.create(start=self.now-timedelta(days=10)) - self.assertEqual(TimeFrame.timeframed.count(), 1) - - - def test_no_start(self): - TimeFrame.objects.create(end=self.now+timedelta(days=2)) - self.assertEqual(TimeFrame.timeframed.count(), 1) - - - def test_within_range(self): - TimeFrame.objects.create(start=self.now-timedelta(days=1), - end=self.now+timedelta(days=1)) - self.assertEqual(TimeFrame.timeframed.count(), 1) - - - -class TimeFrameManagerAddedTests(TestCase): - def test_manager_available(self): - self.assertTrue(isinstance(TimeFrameManagerAdded.timeframed, QueryManager)) - - - def test_conflict_error(self): - with self.assertRaises(ImproperlyConfigured): - class ErrorModel(TimeFramedModel): - timeframed = models.BooleanField() - - - -class StatusModelTests(TestCase): - def setUp(self): - self.model = Status - self.on_hold = Status.STATUS.on_hold - self.active = Status.STATUS.active - - - def test_created(self): - with freeze_time(datetime(2016, 1, 1)): - c1 = self.model.objects.create() - self.assertTrue(c1.status_changed, datetime(2016, 1, 1)) - - c2 = self.model.objects.create() - self.assertEqual(self.model.active.count(), 2) - self.assertEqual(self.model.deleted.count(), 0) - - - def test_modification(self): - t1 = self.model.objects.create() - date_created = t1.status_changed - t1.status = self.on_hold - t1.save() - self.assertEqual(self.model.active.count(), 0) - self.assertEqual(self.model.on_hold.count(), 1) - self.assertTrue(t1.status_changed > date_created) - date_changed = t1.status_changed - t1.save() - self.assertEqual(t1.status_changed, date_changed) - date_active_again = t1.status_changed - t1.status = self.active - t1.save() - self.assertTrue(t1.status_changed > date_active_again) - - - -class StatusModelPlainTupleTests(StatusModelTests): - def setUp(self): - self.model = StatusPlainTuple - self.on_hold = StatusPlainTuple.STATUS[2][0] - self.active = StatusPlainTuple.STATUS[0][0] - - - -class StatusManagerAddedTests(TestCase): - def test_manager_available(self): - self.assertTrue(isinstance(StatusManagerAdded.active, QueryManager)) - - - def test_conflict_error(self): - with self.assertRaises(ImproperlyConfigured): - class ErrorModel(StatusModel): - STATUS = ( - ('active', 'Is Active'), - ('deleted', 'Is Deleted'), - ) - active = models.BooleanField() - - - -class QueryManagerTests(TestCase): - def setUp(self): - data = ((True, True, 0), - (True, False, 4), - (False, False, 2), - (False, True, 3), - (True, True, 1), - (True, False, 5)) - for p, c, o in data: - Post.objects.create(published=p, confirmed=c, order=o) - - - def test_passing_kwargs(self): - qs = Post.public.all() - self.assertEqual([p.order for p in qs], [0, 1, 4, 5]) - - - def test_passing_Q(self): - qs = Post.public_confirmed.all() - self.assertEqual([p.order for p in qs], [0, 1]) - - - def test_ordering(self): - qs = Post.public_reversed.all() - self.assertEqual([p.order for p in qs], [5, 4, 1, 0]) - - - -try: - from south.modelsinspector import introspector -except ImportError: - introspector = None - -@skipUnless(introspector, 'South is not installed') -class SouthFreezingTests(TestCase): - def test_introspector_adds_no_excerpt_field(self): - mf = Article._meta.get_field('body') - args, kwargs = introspector(mf) - self.assertEqual(kwargs['no_excerpt_field'], 'True') - - - def test_no_excerpt_field_works(self): - from .models import NoRendered - with self.assertRaises(FieldDoesNotExist): - NoRendered._meta.get_field('_body_excerpt') - - def test_status_field_no_check_for_status(self): - sf = StatusFieldDefaultFilled._meta.get_field('status') - args, kwargs = introspector(sf) - self.assertEqual(kwargs['no_check_for_status'], 'True') - - -class FieldTrackerTestCase(TestCase): - - tracker = None - - def assertHasChanged(self, **kwargs): - tracker = kwargs.pop('tracker', self.tracker) - for field, value in kwargs.items(): - if value is None: - with self.assertRaises(FieldError): - tracker.has_changed(field) - else: - self.assertEqual(tracker.has_changed(field), value) - - def assertPrevious(self, **kwargs): - tracker = kwargs.pop('tracker', self.tracker) - for field, value in kwargs.items(): - self.assertEqual(tracker.previous(field), value) - - def assertChanged(self, **kwargs): - tracker = kwargs.pop('tracker', self.tracker) - self.assertEqual(tracker.changed(), kwargs) - - def assertCurrent(self, **kwargs): - tracker = kwargs.pop('tracker', self.tracker) - self.assertEqual(tracker.current(), kwargs) - - def update_instance(self, **kwargs): - for field, value in kwargs.items(): - setattr(self.instance, field, value) - self.instance.save() - - -class FieldTrackerCommonTests(object): - - def test_pre_save_previous(self): - self.assertPrevious(name=None, number=None) - self.instance.name = 'new age' - self.instance.number = 8 - self.assertPrevious(name=None, number=None) - - -class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): - - tracked_class = Tracked - - def setUp(self): - self.instance = self.tracked_class() - self.tracker = self.instance.tracker - - def test_descriptor(self): - self.assertTrue(isinstance(self.tracked_class.tracker, FieldTracker)) - - def test_pre_save_changed(self): - self.assertChanged(name=None) - self.instance.name = 'new age' - self.assertChanged(name=None) - self.instance.number = 8 - self.assertChanged(name=None, number=None) - self.instance.name = '' - self.assertChanged(name=None, number=None) - self.instance.mutable = [1,2,3] - self.assertChanged(name=None, number=None, mutable=None) - - def test_pre_save_has_changed(self): - self.assertHasChanged(name=True, number=False, mutable=False) - self.instance.name = 'new age' - self.assertHasChanged(name=True, number=False, mutable=False) - self.instance.number = 7 - self.assertHasChanged(name=True, number=True) - self.instance.mutable = [1,2,3] - self.assertHasChanged(name=True, number=True, mutable=True) - - def test_first_save(self): - self.assertHasChanged(name=True, number=False, mutable=False) - self.assertPrevious(name=None, number=None, mutable=None) - self.assertCurrent(name='', number=None, id=None, mutable=None) - self.assertChanged(name=None) - self.instance.name = 'retro' - self.instance.number = 4 - self.instance.mutable = [1,2,3] - self.assertHasChanged(name=True, number=True, mutable=True) - self.assertPrevious(name=None, number=None, mutable=None) - self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) - self.assertChanged(name=None, number=None, mutable=None) - # Django 1.4 doesn't have update_fields - if django.VERSION >= (1, 5, 0): - self.instance.save(update_fields=[]) - self.assertHasChanged(name=True, number=True, mutable=True) - self.assertPrevious(name=None, number=None, mutable=None) - self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) - self.assertChanged(name=None, number=None, mutable=None) - with self.assertRaises(ValueError): - self.instance.save(update_fields=['number']) - - def test_post_save_has_changed(self): - self.update_instance(name='retro', number=4, mutable=[1,2,3]) - self.assertHasChanged(name=False, number=False, mutable=False) - self.instance.name = 'new age' - self.assertHasChanged(name=True, number=False) - self.instance.number = 8 - self.assertHasChanged(name=True, number=True) - self.instance.mutable[1] = 4 - self.assertHasChanged(name=True, number=True, mutable=True) - self.instance.name = 'retro' - self.assertHasChanged(name=False, number=True, mutable=True) - - def test_post_save_previous(self): - self.update_instance(name='retro', number=4, mutable=[1,2,3]) - self.instance.name = 'new age' - self.assertPrevious(name='retro', number=4, mutable=[1,2,3]) - self.instance.mutable[1] = 4 - self.assertPrevious(name='retro', number=4, mutable=[1,2,3]) - - def test_post_save_changed(self): - self.update_instance(name='retro', number=4, mutable=[1,2,3]) - self.assertChanged() - self.instance.name = 'new age' - self.assertChanged(name='retro') - self.instance.number = 8 - self.assertChanged(name='retro', number=4) - self.instance.name = 'retro' - self.assertChanged(number=4) - self.instance.mutable[1] = 4 - self.assertChanged(number=4, mutable=[1,2,3]) - self.instance.mutable = [1,2,3] - self.assertChanged(number=4) - - def test_current(self): - self.assertCurrent(id=None, name='', number=None, mutable=None) - self.instance.name = 'new age' - self.assertCurrent(id=None, name='new age', number=None, mutable=None) - self.instance.number = 8 - self.assertCurrent(id=None, name='new age', number=8, mutable=None) - self.instance.mutable = [1,2,3] - self.assertCurrent(id=None, name='new age', number=8, mutable=[1,2,3]) - self.instance.mutable[1] = 4 - self.assertCurrent(id=None, name='new age', number=8, mutable=[1,4,3]) - self.instance.save() - self.assertCurrent(id=self.instance.id, name='new age', number=8, mutable=[1,4,3]) - - @skipUnless( - django.VERSION >= (1, 5, 0), "Django 1.4 doesn't have update_fields") - def test_update_fields(self): - self.update_instance(name='retro', number=4, mutable=[1,2,3]) - self.assertChanged() - self.instance.name = 'new age' - self.instance.number = 8 - self.instance.mutable = [4,5,6] - self.assertChanged(name='retro', number=4, mutable=[1,2,3]) - self.instance.save(update_fields=[]) - self.assertChanged(name='retro', number=4, mutable=[1,2,3]) - self.instance.save(update_fields=['name']) - in_db = self.tracked_class.objects.get(id=self.instance.id) - self.assertEqual(in_db.name, self.instance.name) - self.assertNotEqual(in_db.number, self.instance.number) - self.assertChanged(number=4, mutable=[1,2,3]) - self.instance.save(update_fields=['number']) - self.assertChanged(mutable=[1,2,3]) - self.instance.save(update_fields=['mutable']) - self.assertChanged() - in_db = self.tracked_class.objects.get(id=self.instance.id) - self.assertEqual(in_db.name, self.instance.name) - self.assertEqual(in_db.number, self.instance.number) - self.assertEqual(in_db.mutable, self.instance.mutable) - - def test_with_deferred(self): - self.instance.name = 'new age' - self.instance.number = 1 - self.instance.save() - item = list(self.tracked_class.objects.only('name').all())[0] - self.assertTrue(item._deferred_fields) - - self.assertEqual(item.tracker.previous('number'), None) - self.assertTrue('number' in item._deferred_fields) - - self.assertEqual(item.number, 1) - self.assertTrue('number' not in item._deferred_fields) - self.assertEqual(item.tracker.previous('number'), 1) - self.assertFalse(item.tracker.has_changed('number')) - - item.number = 2 - self.assertTrue(item.tracker.has_changed('number')) - - -class FieldTrackerMultipleInstancesTests(TestCase): - - def test_with_deferred_fields_access_multiple(self): - Tracked.objects.create(pk=1, name='foo', number=1) - Tracked.objects.create(pk=2, name='bar', number=2) - - queryset = Tracked.objects.only('id') - - for instance in queryset: - instance.name - - -class FieldTrackedModelCustomTests(FieldTrackerTestCase, - FieldTrackerCommonTests): - - tracked_class = TrackedNotDefault - - def setUp(self): - self.instance = self.tracked_class() - self.tracker = self.instance.name_tracker - - def test_pre_save_changed(self): - self.assertChanged(name=None) - self.instance.name = 'new age' - self.assertChanged(name=None) - self.instance.number = 8 - self.assertChanged(name=None) - self.instance.name = '' - self.assertChanged(name=None) - - def test_first_save(self): - self.assertHasChanged(name=True, number=None) - self.assertPrevious(name=None, number=None) - self.assertCurrent(name='') - self.assertChanged(name=None) - self.instance.name = 'retro' - self.instance.number = 4 - self.assertHasChanged(name=True, number=None) - self.assertPrevious(name=None, number=None) - self.assertCurrent(name='retro') - self.assertChanged(name=None) - - def test_pre_save_has_changed(self): - self.assertHasChanged(name=True, number=None) - self.instance.name = 'new age' - self.assertHasChanged(name=True, number=None) - self.instance.number = 7 - self.assertHasChanged(name=True, number=None) - - def test_post_save_has_changed(self): - self.update_instance(name='retro', number=4) - self.assertHasChanged(name=False, number=None) - self.instance.name = 'new age' - self.assertHasChanged(name=True, number=None) - self.instance.number = 8 - self.assertHasChanged(name=True, number=None) - self.instance.name = 'retro' - self.assertHasChanged(name=False, number=None) - - def test_post_save_previous(self): - self.update_instance(name='retro', number=4) - self.instance.name = 'new age' - self.assertPrevious(name='retro', number=None) - - def test_post_save_changed(self): - self.update_instance(name='retro', number=4) - self.assertChanged() - self.instance.name = 'new age' - self.assertChanged(name='retro') - self.instance.number = 8 - self.assertChanged(name='retro') - self.instance.name = 'retro' - self.assertChanged() - - def test_current(self): - self.assertCurrent(name='') - self.instance.name = 'new age' - self.assertCurrent(name='new age') - self.instance.number = 8 - self.assertCurrent(name='new age') - self.instance.save() - self.assertCurrent(name='new age') - - @skipUnless( - django.VERSION >= (1, 5, 0), "Django 1.4 doesn't have update_fields") - def test_update_fields(self): - self.update_instance(name='retro', number=4) - self.assertChanged() - self.instance.name = 'new age' - self.instance.number = 8 - self.instance.save(update_fields=['name', 'number']) - self.assertChanged() - - -class FieldTrackedModelAttributeTests(FieldTrackerTestCase): - - tracked_class = TrackedNonFieldAttr - - def setUp(self): - self.instance = self.tracked_class() - self.tracker = self.instance.tracker - - def test_previous(self): - self.assertPrevious(rounded=None) - self.instance.number = 7.5 - self.assertPrevious(rounded=None) - self.instance.save() - self.assertPrevious(rounded=8) - self.instance.number = 7.2 - self.assertPrevious(rounded=8) - self.instance.save() - self.assertPrevious(rounded=7) - - def test_has_changed(self): - self.assertHasChanged(rounded=False) - self.instance.number = 7.5 - self.assertHasChanged(rounded=True) - self.instance.save() - self.assertHasChanged(rounded=False) - self.instance.number = 7.2 - self.assertHasChanged(rounded=True) - self.instance.number = 7.8 - self.assertHasChanged(rounded=False) - - def test_changed(self): - self.assertChanged() - self.instance.number = 7.5 - self.assertPrevious(rounded=None) - self.instance.save() - self.assertPrevious() - self.instance.number = 7.8 - self.assertPrevious() - self.instance.number = 7.2 - self.assertPrevious(rounded=8) - self.instance.save() - self.assertPrevious() - - def test_current(self): - self.assertCurrent(rounded=None) - self.instance.number = 7.5 - self.assertCurrent(rounded=8) - self.instance.save() - self.assertCurrent(rounded=8) - - -class FieldTrackedModelMultiTests(FieldTrackerTestCase, - FieldTrackerCommonTests): - - tracked_class = TrackedMultiple - - def setUp(self): - self.instance = self.tracked_class() - self.trackers = [self.instance.name_tracker, - self.instance.number_tracker] - - def test_pre_save_changed(self): - self.tracker = self.instance.name_tracker - self.assertChanged(name=None) - self.instance.name = 'new age' - self.assertChanged(name=None) - self.instance.number = 8 - self.assertChanged(name=None) - self.instance.name = '' - self.assertChanged(name=None) - self.tracker = self.instance.number_tracker - self.assertChanged(number=None) - self.instance.name = 'new age' - self.assertChanged(number=None) - self.instance.number = 8 - self.assertChanged(number=None) - - def test_pre_save_has_changed(self): - self.tracker = self.instance.name_tracker - self.assertHasChanged(name=True, number=None) - self.instance.name = 'new age' - self.assertHasChanged(name=True, number=None) - self.tracker = self.instance.number_tracker - self.assertHasChanged(name=None, number=False) - self.instance.name = 'new age' - self.assertHasChanged(name=None, number=False) - - def test_pre_save_previous(self): - for tracker in self.trackers: - self.tracker = tracker - super(FieldTrackedModelMultiTests, self).test_pre_save_previous() - - def test_post_save_has_changed(self): - self.update_instance(name='retro', number=4) - self.assertHasChanged(tracker=self.trackers[0], name=False, number=None) - self.assertHasChanged(tracker=self.trackers[1], name=None, number=False) - self.instance.name = 'new age' - self.assertHasChanged(tracker=self.trackers[0], name=True, number=None) - self.assertHasChanged(tracker=self.trackers[1], name=None, number=False) - self.instance.number = 8 - self.assertHasChanged(tracker=self.trackers[0], name=True, number=None) - self.assertHasChanged(tracker=self.trackers[1], name=None, number=True) - self.instance.name = 'retro' - self.instance.number = 4 - self.assertHasChanged(tracker=self.trackers[0], name=False, number=None) - self.assertHasChanged(tracker=self.trackers[1], name=None, number=False) - - def test_post_save_previous(self): - self.update_instance(name='retro', number=4) - self.instance.name = 'new age' - self.instance.number = 8 - self.assertPrevious(tracker=self.trackers[0], name='retro', number=None) - self.assertPrevious(tracker=self.trackers[1], name=None, number=4) - - def test_post_save_changed(self): - self.update_instance(name='retro', number=4) - self.assertChanged(tracker=self.trackers[0]) - self.assertChanged(tracker=self.trackers[1]) - self.instance.name = 'new age' - self.assertChanged(tracker=self.trackers[0], name='retro') - self.assertChanged(tracker=self.trackers[1]) - self.instance.number = 8 - self.assertChanged(tracker=self.trackers[0], name='retro') - self.assertChanged(tracker=self.trackers[1], number=4) - self.instance.name = 'retro' - self.instance.number = 4 - self.assertChanged(tracker=self.trackers[0]) - self.assertChanged(tracker=self.trackers[1]) - - def test_current(self): - self.assertCurrent(tracker=self.trackers[0], name='') - self.assertCurrent(tracker=self.trackers[1], number=None) - self.instance.name = 'new age' - self.assertCurrent(tracker=self.trackers[0], name='new age') - self.assertCurrent(tracker=self.trackers[1], number=None) - self.instance.number = 8 - self.assertCurrent(tracker=self.trackers[0], name='new age') - self.assertCurrent(tracker=self.trackers[1], number=8) - self.instance.save() - self.assertCurrent(tracker=self.trackers[0], name='new age') - self.assertCurrent(tracker=self.trackers[1], number=8) - - -class FieldTrackerForeignKeyTests(FieldTrackerTestCase): - - fk_class = Tracked - tracked_class = TrackedFK - - def setUp(self): - self.old_fk = self.fk_class.objects.create(number=8) - self.instance = self.tracked_class.objects.create(fk=self.old_fk) - - def test_default(self): - self.tracker = self.instance.tracker - self.assertChanged() - self.assertPrevious() - self.assertCurrent(id=self.instance.id, fk_id=self.old_fk.id) - self.instance.fk = self.fk_class.objects.create(number=8) - self.assertChanged(fk_id=self.old_fk.id) - self.assertPrevious(fk_id=self.old_fk.id) - self.assertCurrent(id=self.instance.id, fk_id=self.instance.fk_id) - - def test_custom(self): - self.tracker = self.instance.custom_tracker - self.assertChanged() - self.assertPrevious() - self.assertCurrent(fk_id=self.old_fk.id) - self.instance.fk = self.fk_class.objects.create(number=8) - self.assertChanged(fk_id=self.old_fk.id) - self.assertPrevious(fk_id=self.old_fk.id) - self.assertCurrent(fk_id=self.instance.fk_id) - - def test_custom_without_id(self): - with self.assertNumQueries(1): - self.tracked_class.objects.get() - self.tracker = self.instance.custom_tracker_without_id - self.assertChanged() - self.assertPrevious() - self.assertCurrent(fk=self.old_fk.id) - self.instance.fk = self.fk_class.objects.create(number=8) - self.assertChanged(fk=self.old_fk.id) - self.assertPrevious(fk=self.old_fk.id) - self.assertCurrent(fk=self.instance.fk_id) - - -class InheritedFieldTrackerTests(FieldTrackerTests): - - tracked_class = InheritedTracked - - def test_child_fields_not_tracked(self): - self.name2 = 'test' - self.assertEqual(self.tracker.previous('name2'), None) - self.assertRaises(FieldError, self.tracker.has_changed, 'name2') - - -class FieldTrackerInheritedForeignKeyTests(FieldTrackerForeignKeyTests): - - tracked_class = InheritedTrackedFK - - -class FieldTrackerFileFieldTests(FieldTrackerTestCase): - - tracked_class = TrackedFileField - - def setUp(self): - self.instance = self.tracked_class() - self.tracker = self.instance.tracker - self.some_file = 'something.txt' - self.another_file = 'another.txt' - - def test_pre_save_changed(self): - self.assertChanged(some_file=None) - self.instance.some_file = self.some_file - self.assertChanged(some_file=None) - - def test_pre_save_has_changed(self): - self.assertHasChanged(some_file=True) - self.instance.some_file = self.some_file - self.assertHasChanged(some_file=True) - - def test_pre_save_previous(self): - self.assertPrevious(some_file=None) - self.instance.some_file = self.some_file - self.assertPrevious(some_file=None) - - def test_post_save_changed(self): - self.update_instance(some_file=self.some_file) - self.assertChanged() - previous_file = self.instance.some_file - self.instance.some_file = self.another_file - self.assertChanged(some_file=previous_file) - # test deferred file field - deferred_instance = self.tracked_class.objects.defer('some_file')[0] - deferred_instance.some_file # access field to fetch from database - self.assertChanged(tracker=deferred_instance.tracker) - - previous_file = deferred_instance.some_file - deferred_instance.some_file = self.another_file - self.assertChanged( - tracker=deferred_instance.tracker, - some_file=previous_file, - ) - - def test_post_save_has_changed(self): - self.update_instance(some_file=self.some_file) - self.assertHasChanged(some_file=False) - self.instance.some_file = self.another_file - self.assertHasChanged(some_file=True) - - # test deferred file field - deferred_instance = self.tracked_class.objects.defer('some_file')[0] - deferred_instance.some_file # access field to fetch from database - self.assertHasChanged( - tracker=deferred_instance.tracker, - some_file=False, - ) - - deferred_instance.some_file = self.another_file - self.assertHasChanged( - tracker=deferred_instance.tracker, - some_file=True, - ) - - def test_post_save_previous(self): - self.update_instance(some_file=self.some_file) - previous_file = self.instance.some_file - self.instance.some_file = self.another_file - self.assertPrevious(some_file=previous_file) - - # test deferred file field - deferred_instance = self.tracked_class.objects.defer('some_file')[0] - deferred_instance.some_file # access field to fetch from database - self.assertPrevious( - tracker=deferred_instance.tracker, - some_file=previous_file, - ) - - deferred_instance.some_file = self.another_file - self.assertPrevious( - tracker=deferred_instance.tracker, - some_file=previous_file, - ) - - def test_current(self): - self.assertCurrent(some_file=self.instance.some_file, id=None) - self.instance.some_file = self.some_file - self.assertCurrent(some_file=self.instance.some_file, id=None) - - # test deferred file field - self.instance.save() - deferred_instance = self.tracked_class.objects.defer('some_file')[0] - deferred_instance.some_file # access field to fetch from database - self.assertCurrent( - some_file=self.instance.some_file, - id=self.instance.id, - ) - - self.instance.some_file = self.another_file - self.assertCurrent( - some_file=self.instance.some_file, - id=self.instance.id, - ) - - -class ModelTrackerTests(FieldTrackerTests): - - tracked_class = ModelTracked - - def test_pre_save_changed(self): - self.assertChanged() - self.instance.name = 'new age' - self.assertChanged() - self.instance.number = 8 - self.assertChanged() - self.instance.name = '' - self.assertChanged() - self.instance.mutable = [1,2,3] - self.assertChanged() - - def test_first_save(self): - self.assertHasChanged(name=True, number=True, mutable=True) - self.assertPrevious(name=None, number=None, mutable=None) - self.assertCurrent(name='', number=None, id=None, mutable=None) - self.assertChanged() - self.instance.name = 'retro' - self.instance.number = 4 - self.instance.mutable = [1,2,3] - self.assertHasChanged(name=True, number=True, mutable=True) - self.assertPrevious(name=None, number=None, mutable=None) - self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) - self.assertChanged() - # Django 1.4 doesn't have update_fields - if django.VERSION >= (1, 5, 0): - self.instance.save(update_fields=[]) - self.assertHasChanged(name=True, number=True, mutable=True) - self.assertPrevious(name=None, number=None, mutable=None) - self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) - self.assertChanged() - with self.assertRaises(ValueError): - self.instance.save(update_fields=['number']) - - def test_pre_save_has_changed(self): - self.assertHasChanged(name=True, number=True) - self.instance.name = 'new age' - self.assertHasChanged(name=True, number=True) - self.instance.number = 7 - self.assertHasChanged(name=True, number=True) - - -class ModelTrackedModelCustomTests(FieldTrackedModelCustomTests): - - tracked_class = ModelTrackedNotDefault - - def test_first_save(self): - self.assertHasChanged(name=True, number=True) - self.assertPrevious(name=None, number=None) - self.assertCurrent(name='') - self.assertChanged() - self.instance.name = 'retro' - self.instance.number = 4 - self.assertHasChanged(name=True, number=True) - self.assertPrevious(name=None, number=None) - self.assertCurrent(name='retro') - self.assertChanged() - - def test_pre_save_has_changed(self): - self.assertHasChanged(name=True, number=True) - self.instance.name = 'new age' - self.assertHasChanged(name=True, number=True) - self.instance.number = 7 - self.assertHasChanged(name=True, number=True) - - def test_pre_save_changed(self): - self.assertChanged() - self.instance.name = 'new age' - self.assertChanged() - self.instance.number = 8 - self.assertChanged() - self.instance.name = '' - self.assertChanged() - - -class ModelTrackedModelMultiTests(FieldTrackedModelMultiTests): - - tracked_class = ModelTrackedMultiple - - def test_pre_save_has_changed(self): - self.tracker = self.instance.name_tracker - self.assertHasChanged(name=True, number=True) - self.instance.name = 'new age' - self.assertHasChanged(name=True, number=True) - self.tracker = self.instance.number_tracker - self.assertHasChanged(name=True, number=True) - self.instance.name = 'new age' - self.assertHasChanged(name=True, number=True) - - def test_pre_save_changed(self): - self.tracker = self.instance.name_tracker - self.assertChanged() - self.instance.name = 'new age' - self.assertChanged() - self.instance.number = 8 - self.assertChanged() - self.instance.name = '' - self.assertChanged() - self.tracker = self.instance.number_tracker - self.assertChanged() - self.instance.name = 'new age' - self.assertChanged() - self.instance.number = 8 - self.assertChanged() - - -class ModelTrackerForeignKeyTests(FieldTrackerForeignKeyTests): - - fk_class = ModelTracked - tracked_class = ModelTrackedFK - - def test_custom_without_id(self): - with self.assertNumQueries(2): - self.tracked_class.objects.get() - self.tracker = self.instance.custom_tracker_without_id - self.assertChanged() - self.assertPrevious() - self.assertCurrent(fk=self.old_fk) - self.instance.fk = self.fk_class.objects.create(number=8) - self.assertNotEqual(self.instance.fk, self.old_fk) - self.assertChanged(fk=self.old_fk) - self.assertPrevious(fk=self.old_fk) - self.assertCurrent(fk=self.instance.fk) - - -class InheritedModelTrackerTests(ModelTrackerTests): - - tracked_class = InheritedModelTracked - - def test_child_fields_not_tracked(self): - self.name2 = 'test' - self.assertEqual(self.tracker.previous('name2'), None) - self.assertTrue(self.tracker.has_changed('name2')) - - -class SoftDeletableModelTests(TestCase): - - def test_can_only_see_not_removed_entries(self): - SoftDeletable.objects.create(name='a', is_removed=True) - SoftDeletable.objects.create(name='b', is_removed=False) - - queryset = SoftDeletable.objects.all() - - self.assertEqual(queryset.count(), 1) - self.assertEqual(queryset[0].name, 'b') - - def test_instance_cannot_be_fully_deleted(self): - instance = SoftDeletable.objects.create(name='a') - - instance.delete() - - self.assertEqual(SoftDeletable.objects.count(), 0) - self.assertEqual(SoftDeletable.all_objects.count(), 1) - - def test_instance_cannot_be_fully_deleted_via_queryset(self): - SoftDeletable.objects.create(name='a') - - SoftDeletable.objects.all().delete() - - self.assertEqual(SoftDeletable.objects.count(), 0) - self.assertEqual(SoftDeletable.all_objects.count(), 1) - - def test_delete_instance_no_connection(self): - obj = SoftDeletable.objects.create(name='a') - - self.assertRaises(ConnectionDoesNotExist, obj.delete, using='other') +# Needed for Django 1.4/1.5 test runner +from .test_fields import * +from .test_managers import * +from .test_models import * +from .test_choices import * +from .test_miscellaneous import * From 2a9c19981921002b319201d86184fd0fe0e03f2f Mon Sep 17 00:00:00 2001 From: romgar Date: Thu, 24 Nov 2016 21:31:23 +0000 Subject: [PATCH 117/271] Remove test duplication related to TestCase inheritance in different files --- .../tests/test_fields/test_field_tracker.py | 142 ++++++++++++++++- model_utils/tests/test_models/__init__.py | 1 - .../tests/test_models/test_model_tracker.py | 150 ------------------ 3 files changed, 141 insertions(+), 152 deletions(-) delete mode 100644 model_utils/tests/test_models/test_model_tracker.py diff --git a/model_utils/tests/test_fields/test_field_tracker.py b/model_utils/tests/test_fields/test_field_tracker.py index 4bd96e1..c215f77 100644 --- a/model_utils/tests/test_fields/test_field_tracker.py +++ b/model_utils/tests/test_fields/test_field_tracker.py @@ -8,7 +8,9 @@ from model_utils import FieldTracker from model_utils.tests.helpers import skipUnless from model_utils.tests.models import ( Tracked, TrackedFK, InheritedTrackedFK, TrackedNotDefault, TrackedNonFieldAttr, TrackedMultiple, - InheritedTracked, TrackedFileField) + InheritedTracked, TrackedFileField, + ModelTracked, ModelTrackedFK, ModelTrackedNotDefault, ModelTrackedMultiple, InheritedModelTracked, +) class FieldTrackerTestCase(TestCase): @@ -591,3 +593,141 @@ class FieldTrackerFileFieldTests(FieldTrackerTestCase): some_file=self.instance.some_file, id=self.instance.id, ) + + +class ModelTrackerTests(FieldTrackerTests): + + tracked_class = ModelTracked + + def test_pre_save_changed(self): + self.assertChanged() + self.instance.name = 'new age' + self.assertChanged() + self.instance.number = 8 + self.assertChanged() + self.instance.name = '' + self.assertChanged() + self.instance.mutable = [1,2,3] + self.assertChanged() + + def test_first_save(self): + self.assertHasChanged(name=True, number=True, mutable=True) + self.assertPrevious(name=None, number=None, mutable=None) + self.assertCurrent(name='', number=None, id=None, mutable=None) + self.assertChanged() + self.instance.name = 'retro' + self.instance.number = 4 + self.instance.mutable = [1,2,3] + self.assertHasChanged(name=True, number=True, mutable=True) + self.assertPrevious(name=None, number=None, mutable=None) + self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) + self.assertChanged() + # Django 1.4 doesn't have update_fields + if django.VERSION >= (1, 5, 0): + self.instance.save(update_fields=[]) + self.assertHasChanged(name=True, number=True, mutable=True) + self.assertPrevious(name=None, number=None, mutable=None) + self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) + self.assertChanged() + with self.assertRaises(ValueError): + self.instance.save(update_fields=['number']) + + def test_pre_save_has_changed(self): + self.assertHasChanged(name=True, number=True) + self.instance.name = 'new age' + self.assertHasChanged(name=True, number=True) + self.instance.number = 7 + self.assertHasChanged(name=True, number=True) + + +class ModelTrackedModelCustomTests(FieldTrackedModelCustomTests): + + tracked_class = ModelTrackedNotDefault + + def test_first_save(self): + self.assertHasChanged(name=True, number=True) + self.assertPrevious(name=None, number=None) + self.assertCurrent(name='') + self.assertChanged() + self.instance.name = 'retro' + self.instance.number = 4 + self.assertHasChanged(name=True, number=True) + self.assertPrevious(name=None, number=None) + self.assertCurrent(name='retro') + self.assertChanged() + + def test_pre_save_has_changed(self): + self.assertHasChanged(name=True, number=True) + self.instance.name = 'new age' + self.assertHasChanged(name=True, number=True) + self.instance.number = 7 + self.assertHasChanged(name=True, number=True) + + def test_pre_save_changed(self): + self.assertChanged() + self.instance.name = 'new age' + self.assertChanged() + self.instance.number = 8 + self.assertChanged() + self.instance.name = '' + self.assertChanged() + + +class ModelTrackedModelMultiTests(FieldTrackedModelMultiTests): + + tracked_class = ModelTrackedMultiple + + def test_pre_save_has_changed(self): + self.tracker = self.instance.name_tracker + self.assertHasChanged(name=True, number=True) + self.instance.name = 'new age' + self.assertHasChanged(name=True, number=True) + self.tracker = self.instance.number_tracker + self.assertHasChanged(name=True, number=True) + self.instance.name = 'new age' + self.assertHasChanged(name=True, number=True) + + def test_pre_save_changed(self): + self.tracker = self.instance.name_tracker + self.assertChanged() + self.instance.name = 'new age' + self.assertChanged() + self.instance.number = 8 + self.assertChanged() + self.instance.name = '' + self.assertChanged() + self.tracker = self.instance.number_tracker + self.assertChanged() + self.instance.name = 'new age' + self.assertChanged() + self.instance.number = 8 + self.assertChanged() + + +class ModelTrackerForeignKeyTests(FieldTrackerForeignKeyTests): + + fk_class = ModelTracked + tracked_class = ModelTrackedFK + + def test_custom_without_id(self): + with self.assertNumQueries(2): + self.tracked_class.objects.get() + self.tracker = self.instance.custom_tracker_without_id + self.assertChanged() + self.assertPrevious() + self.assertCurrent(fk=self.old_fk) + self.instance.fk = self.fk_class.objects.create(number=8) + self.assertNotEqual(self.instance.fk, self.old_fk) + self.assertChanged(fk=self.old_fk) + self.assertPrevious(fk=self.old_fk) + self.assertCurrent(fk=self.instance.fk) + + +class InheritedModelTrackerTests(ModelTrackerTests): + + tracked_class = InheritedModelTracked + + def test_child_fields_not_tracked(self): + self.name2 = 'test' + self.assertEqual(self.tracker.previous('name2'), None) + self.assertTrue(self.tracker.has_changed('name2')) diff --git a/model_utils/tests/test_models/__init__.py b/model_utils/tests/test_models/__init__.py index 5065567..6a0970b 100644 --- a/model_utils/tests/test_models/__init__.py +++ b/model_utils/tests/test_models/__init__.py @@ -1,5 +1,4 @@ # Needed for Django 1.4/1.5 test runner -from .test_model_tracker import * from .test_softdeletable_model import * from .test_status_model import * from .test_timeframed_model import * diff --git a/model_utils/tests/test_models/test_model_tracker.py b/model_utils/tests/test_models/test_model_tracker.py deleted file mode 100644 index c9448d1..0000000 --- a/model_utils/tests/test_models/test_model_tracker.py +++ /dev/null @@ -1,150 +0,0 @@ -from __future__ import unicode_literals - -import django - -from model_utils.tests.models import ( - ModelTracked, ModelTrackedFK, ModelTrackedNotDefault, ModelTrackedMultiple, InheritedModelTracked, -) - -from model_utils.tests.test_fields.test_field_tracker import ( - FieldTrackerTests, FieldTrackedModelCustomTests, - FieldTrackedModelMultiTests, FieldTrackerForeignKeyTests -) - - -class ModelTrackerTests(FieldTrackerTests): - - tracked_class = ModelTracked - - def test_pre_save_changed(self): - self.assertChanged() - self.instance.name = 'new age' - self.assertChanged() - self.instance.number = 8 - self.assertChanged() - self.instance.name = '' - self.assertChanged() - self.instance.mutable = [1,2,3] - self.assertChanged() - - def test_first_save(self): - self.assertHasChanged(name=True, number=True, mutable=True) - self.assertPrevious(name=None, number=None, mutable=None) - self.assertCurrent(name='', number=None, id=None, mutable=None) - self.assertChanged() - self.instance.name = 'retro' - self.instance.number = 4 - self.instance.mutable = [1,2,3] - self.assertHasChanged(name=True, number=True, mutable=True) - self.assertPrevious(name=None, number=None, mutable=None) - self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) - self.assertChanged() - # Django 1.4 doesn't have update_fields - if django.VERSION >= (1, 5, 0): - self.instance.save(update_fields=[]) - self.assertHasChanged(name=True, number=True, mutable=True) - self.assertPrevious(name=None, number=None, mutable=None) - self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) - self.assertChanged() - with self.assertRaises(ValueError): - self.instance.save(update_fields=['number']) - - def test_pre_save_has_changed(self): - self.assertHasChanged(name=True, number=True) - self.instance.name = 'new age' - self.assertHasChanged(name=True, number=True) - self.instance.number = 7 - self.assertHasChanged(name=True, number=True) - - -class ModelTrackedModelCustomTests(FieldTrackedModelCustomTests): - - tracked_class = ModelTrackedNotDefault - - def test_first_save(self): - self.assertHasChanged(name=True, number=True) - self.assertPrevious(name=None, number=None) - self.assertCurrent(name='') - self.assertChanged() - self.instance.name = 'retro' - self.instance.number = 4 - self.assertHasChanged(name=True, number=True) - self.assertPrevious(name=None, number=None) - self.assertCurrent(name='retro') - self.assertChanged() - - def test_pre_save_has_changed(self): - self.assertHasChanged(name=True, number=True) - self.instance.name = 'new age' - self.assertHasChanged(name=True, number=True) - self.instance.number = 7 - self.assertHasChanged(name=True, number=True) - - def test_pre_save_changed(self): - self.assertChanged() - self.instance.name = 'new age' - self.assertChanged() - self.instance.number = 8 - self.assertChanged() - self.instance.name = '' - self.assertChanged() - - -class ModelTrackedModelMultiTests(FieldTrackedModelMultiTests): - - tracked_class = ModelTrackedMultiple - - def test_pre_save_has_changed(self): - self.tracker = self.instance.name_tracker - self.assertHasChanged(name=True, number=True) - self.instance.name = 'new age' - self.assertHasChanged(name=True, number=True) - self.tracker = self.instance.number_tracker - self.assertHasChanged(name=True, number=True) - self.instance.name = 'new age' - self.assertHasChanged(name=True, number=True) - - def test_pre_save_changed(self): - self.tracker = self.instance.name_tracker - self.assertChanged() - self.instance.name = 'new age' - self.assertChanged() - self.instance.number = 8 - self.assertChanged() - self.instance.name = '' - self.assertChanged() - self.tracker = self.instance.number_tracker - self.assertChanged() - self.instance.name = 'new age' - self.assertChanged() - self.instance.number = 8 - self.assertChanged() - - -class ModelTrackerForeignKeyTests(FieldTrackerForeignKeyTests): - - fk_class = ModelTracked - tracked_class = ModelTrackedFK - - def test_custom_without_id(self): - with self.assertNumQueries(2): - self.tracked_class.objects.get() - self.tracker = self.instance.custom_tracker_without_id - self.assertChanged() - self.assertPrevious() - self.assertCurrent(fk=self.old_fk) - self.instance.fk = self.fk_class.objects.create(number=8) - self.assertNotEqual(self.instance.fk, self.old_fk) - self.assertChanged(fk=self.old_fk) - self.assertPrevious(fk=self.old_fk) - self.assertCurrent(fk=self.instance.fk) - - -class InheritedModelTrackerTests(ModelTrackerTests): - - tracked_class = InheritedModelTracked - - def test_child_fields_not_tracked(self): - self.name2 = 'test' - self.assertEqual(self.tracker.previous('name2'), None) - self.assertTrue(self.tracker.has_changed('name2')) From eb41dc0ea07ac40db447dd446410f115e9d2bb85 Mon Sep 17 00:00:00 2001 From: romgar Date: Thu, 24 Nov 2016 21:31:55 +0000 Subject: [PATCH 118/271] Prevent test manual import for Django 1.6+. No impact on Django 1.7/1.8, duplicating all test runs in Django 1.6 --- model_utils/tests/tests.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index 39eced3..b023bad 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -1,6 +1,9 @@ +import django + # Needed for Django 1.4/1.5 test runner -from .test_fields import * -from .test_managers import * -from .test_models import * -from .test_choices import * -from .test_miscellaneous import * +if django.VERSION < (1, 6): + from .test_fields import * + from .test_managers import * + from .test_models import * + from .test_choices import * + from .test_miscellaneous import * From 4311e24e98fd1939d821cbe2ff375f41cb2b87cb Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Mon, 28 Nov 2016 23:45:48 +0000 Subject: [PATCH 119/271] Add soft parameter to delete method instead of new one --- model_utils/models.py | 22 +++++++------------ .../test_models/test_softdeletable_model.py | 5 +++-- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/model_utils/models.py b/model_utils/models.py index a7876e7..9c022cb 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -115,19 +115,13 @@ class SoftDeletableModel(models.Model): objects = SoftDeletableManager() - def delete(self, using=None, keep_parents=False): + def delete(self, using=None, soft=True, *args, **kwargs): """ - Soft delete object (set its ``is_removed`` field to True) + Soft delete object (set its ``is_removed`` field to True). + Actually delete object if setting ``soft`` to False. """ - self.is_removed = True - self.save(using=using) - - def purge_from_db(self, using=None, keep_parents=False): - """ - Actually purge the entry from the database - """ - del_kwargs = {'using': using} - # keep_parents option is new in Django 1.9 - if django.VERSION >= (1, 9, 0): - del_kwargs['keep_parents'] = keep_parents - super(SoftDeletableModel, self).delete(**del_kwargs) + if soft: + self.is_removed = True + self.save(using=using) + else: + return super(SoftDeletableModel, self).delete(using=using, *args, **kwargs) diff --git a/model_utils/tests/test_models/test_softdeletable_model.py b/model_utils/tests/test_models/test_softdeletable_model.py index ee7f2f1..fb05e08 100644 --- a/model_utils/tests/test_models/test_softdeletable_model.py +++ b/model_utils/tests/test_models/test_softdeletable_model.py @@ -40,7 +40,7 @@ class SoftDeletableModelTests(TestCase): def test_instance_purge(self): instance = SoftDeletable.objects.create(name='a') - instance.purge_from_db() + instance.delete(soft=False) self.assertEqual(SoftDeletable.objects.count(), 0) self.assertEqual(SoftDeletable.all_objects.count(), 0) @@ -48,4 +48,5 @@ class SoftDeletableModelTests(TestCase): def test_instance_purge_no_connection(self): instance = SoftDeletable.objects.create(name='a') - self.assertRaises(ConnectionDoesNotExist, instance.purge_from_db, using='other') + self.assertRaises(ConnectionDoesNotExist, instance.delete, + using='other', soft=False) From 0efaad1218fd95a10de6a14f6ada71ea39c80c1a Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Thu, 5 Jan 2017 14:29:35 +0000 Subject: [PATCH 120/271] Fix issue when extend QuerySet and Manager - fixes #249 --- model_utils/managers.py | 14 ++++--- model_utils/tests/managers.py | 15 +++++++ model_utils/tests/models.py | 41 ++++++------------- model_utils/tests/test_managers/__init__.py | 1 + .../test_managers/test_softdelete_manager.py | 28 +++++++++++++ 5 files changed, 64 insertions(+), 35 deletions(-) create mode 100644 model_utils/tests/managers.py create mode 100644 model_utils/tests/test_managers/test_softdelete_manager.py diff --git a/model_utils/managers.py b/model_utils/managers.py index c6a2989..1c758cd 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -192,11 +192,16 @@ class InheritanceQuerySetMixin(object): return levels +class InheritanceQuerySet(InheritanceQuerySetMixin, QuerySet): + pass + + class InheritanceManagerMixin(object): use_for_related_fields = True + _queryset_class = InheritanceQuerySet def get_queryset(self): - return InheritanceQuerySet(self.model) + return self._queryset_class(self.model) get_query_set = get_queryset @@ -207,10 +212,6 @@ class InheritanceManagerMixin(object): return self.get_queryset().get_subclass(*args, **kwargs) -class InheritanceQuerySet(InheritanceQuerySetMixin, QuerySet): - pass - - class InheritanceManager(InheritanceManagerMixin, models.Manager): pass @@ -265,6 +266,7 @@ class SoftDeletableManager(models.Manager): Manager that limits the queryset by default to show only not removed instances of model. """ + use_for_related_fields = True _queryset_class = SoftDeletableQuerySet def get_queryset(self): @@ -275,6 +277,6 @@ class SoftDeletableManager(models.Manager): if hasattr(self, '_hints'): kwargs['hints'] = self._hints - return SoftDeletableQuerySet(**kwargs).filter(is_removed=False) + return self._queryset_class(**kwargs).filter(is_removed=False) get_query_set = get_queryset diff --git a/model_utils/tests/managers.py b/model_utils/tests/managers.py new file mode 100644 index 0000000..4a055f2 --- /dev/null +++ b/model_utils/tests/managers.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals, absolute_import + +from model_utils.managers import SoftDeletableQuerySet, SoftDeletableManager + + +class CustomSoftDeleteQuerySet(SoftDeletableQuerySet): + def only_read(self): + return self.filter(is_read=True) + + +class CustomSoftDeleteManager(SoftDeletableManager): + _queryset_class = CustomSoftDeleteQuerySet + + def only_read(self): + return self.get_queryset().only_read() diff --git a/model_utils/tests/models.py b/model_utils/tests/models.py index 7876828..c10468d 100644 --- a/model_utils/tests/models.py +++ b/model_utils/tests/models.py @@ -4,25 +4,24 @@ from django.db import models from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ +from model_utils import Choices +from model_utils.fields import SplitField, MonitorField, StatusField +from model_utils.managers import QueryManager, InheritanceManager from model_utils.models import ( SoftDeletableModel, StatusModel, TimeFramedModel, TimeStampedModel, ) -from model_utils.tracker import FieldTracker, ModelTracker -from model_utils.managers import QueryManager, InheritanceManager -from model_utils.fields import SplitField, MonitorField, StatusField from model_utils.tests.fields import MutableField -from model_utils import Choices - +from model_utils.tests.managers import CustomSoftDeleteManager +from model_utils.tracker import FieldTracker, ModelTracker class InheritanceManagerTestRelated(models.Model): pass - @python_2_unicode_compatible class InheritanceManagerTestParent(models.Model): # FileField is just a handy descriptor-using field. Refs #6. @@ -40,8 +39,7 @@ class InheritanceManagerTestParent(models.Model): return "%s(%s)" % ( self.__class__.__name__[len('InheritanceManagerTest'):], self.pk, - ) - + ) class InheritanceManagerTestChild1(InheritanceManagerTestParent): @@ -50,23 +48,19 @@ class InheritanceManagerTestChild1(InheritanceManagerTestParent): objects = InheritanceManager() - class InheritanceManagerTestGrandChild1(InheritanceManagerTestChild1): text_field = models.TextField() - class InheritanceManagerTestGrandChild1_2(InheritanceManagerTestChild1): text_field = models.TextField() - class InheritanceManagerTestChild2(InheritanceManagerTestParent): non_related_field_using_descriptor_2 = models.FileField(upload_to="test") normal_field_2 = models.TextField() - class InheritanceManagerTestChild3(InheritanceManagerTestParent): parent_ptr = models.OneToOneField( InheritanceManagerTestParent, related_name='manual_onetoone', @@ -77,29 +71,24 @@ class TimeStamp(TimeStampedModel): pass - class TimeFrame(TimeFramedModel): pass - class TimeFrameManagerAdded(TimeFramedModel): pass - class Monitored(models.Model): name = models.CharField(max_length=25) name_changed = MonitorField(monitor="name") - class MonitorWhen(models.Model): name = models.CharField(max_length=25) name_changed = MonitorField(monitor="name", when=["Jose", "Maria"]) - class MonitorWhenEmpty(models.Model): name = models.CharField(max_length=25) name_changed = MonitorField(monitor="name", when=[]) @@ -120,7 +109,6 @@ class Status(StatusModel): ) - class StatusPlainTuple(StatusModel): STATUS = ( ("active", _("active")), @@ -129,7 +117,6 @@ class StatusPlainTuple(StatusModel): ) - class StatusManagerAdded(StatusModel): STATUS = ( ("active", _("active")), @@ -138,7 +125,6 @@ class StatusManagerAdded(StatusModel): ) - class Post(models.Model): published = models.BooleanField(default=False) confirmed = models.BooleanField(default=False) @@ -154,22 +140,18 @@ class Post(models.Model): ordering = ("order",) - class Article(models.Model): title = models.CharField(max_length=50) body = SplitField() - class SplitFieldAbstractParent(models.Model): content = SplitField() - class Meta: abstract = True - class NoRendered(models.Model): """ Test that the no_excerpt_field keyword arg works. This arg should @@ -179,29 +161,24 @@ class NoRendered(models.Model): body = SplitField(no_excerpt_field=True) - class AuthorMixin(object): def by_author(self, name): return self.filter(author=name) - class PublishedMixin(object): def published(self): return self.filter(published=True) - def unpublished(self): return self.filter(published=False) - class ByAuthorQuerySet(models.query.QuerySet, AuthorMixin): pass - class FeaturedManager(models.Manager): def get_queryset(self): kwargs = {} @@ -326,3 +303,9 @@ class SoftDeletable(SoftDeletableModel): name = models.CharField(max_length=20) all_objects = models.Manager() + + +class CustomSoftDelete(SoftDeletableModel): + is_read = models.BooleanField(default=False) + + objects = CustomSoftDeleteManager() diff --git a/model_utils/tests/test_managers/__init__.py b/model_utils/tests/test_managers/__init__.py index 0f8aec6..92661aa 100644 --- a/model_utils/tests/test_managers/__init__.py +++ b/model_utils/tests/test_managers/__init__.py @@ -2,3 +2,4 @@ from .test_inheritance_manager import * from .test_query_manager import * from .test_status_manager import * +from .test_softdelete_manager import * diff --git a/model_utils/tests/test_managers/test_softdelete_manager.py b/model_utils/tests/test_managers/test_softdelete_manager.py new file mode 100644 index 0000000..3f5ed47 --- /dev/null +++ b/model_utils/tests/test_managers/test_softdelete_manager.py @@ -0,0 +1,28 @@ +from __future__ import unicode_literals + +from django.test import TestCase + +from model_utils.tests.models import CustomSoftDelete + + +class CustomSoftDeleteManagerTests(TestCase): + + def test_custom_manager_empty(self): + qs = CustomSoftDelete.objects.only_read() + self.assertEqual(qs.count(), 0) + + def test_custom_qs_empty(self): + qs = CustomSoftDelete.objects.all().only_read() + self.assertEqual(qs.count(), 0) + + def test_is_read(self): + for is_read in [True, False, True, False]: + CustomSoftDelete.objects.create(is_read=is_read) + qs = CustomSoftDelete.objects.only_read() + self.assertEqual(qs.count(), 2) + + def test_is_read_removed(self): + for is_read, is_removed in [(True, True), (True, False), (False, False), (False, True)]: + CustomSoftDelete.objects.create(is_read=is_read, is_removed=is_removed) + qs = CustomSoftDelete.objects.only_read() + self.assertEqual(qs.count(), 1) From 063332643dab78c9d4c5c4caac7cddc6ac0d8bb8 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Thu, 5 Jan 2017 14:40:03 +0000 Subject: [PATCH 121/271] Split SoftDeletableQuerySet/Manager into Mixin --- model_utils/managers.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/model_utils/managers.py b/model_utils/managers.py index 1c758cd..685b07d 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -247,7 +247,7 @@ class QueryManager(QueryManagerMixin, models.Manager): pass -class SoftDeletableQuerySet(QuerySet): +class SoftDeletableQuerySetMixin(object): """ QuerySet for SoftDeletableModel. Instead of removing instance sets its ``is_removed`` field to True. @@ -261,7 +261,11 @@ class SoftDeletableQuerySet(QuerySet): self.update(is_removed=True) -class SoftDeletableManager(models.Manager): +class SoftDeletableQuerySet(SoftDeletableQuerySetMixin, QuerySet): + pass + + +class SoftDeletableManagerMixin(object): """ Manager that limits the queryset by default to show only not removed instances of model. @@ -280,3 +284,7 @@ class SoftDeletableManager(models.Manager): return self._queryset_class(**kwargs).filter(is_removed=False) get_query_set = get_queryset + + +class SoftDeletableManager(SoftDeletableManagerMixin, models.Manager): + pass From 2af54972ebe79f4e9b70c4c4902497a363de9e34 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Thu, 5 Jan 2017 18:32:13 +0000 Subject: [PATCH 122/271] Revert unrelated change regarding related fields Remove attribute use_for_related_fields in SoftDeletableManagerMixin. --- model_utils/managers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/model_utils/managers.py b/model_utils/managers.py index 685b07d..0b677ff 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -270,7 +270,6 @@ class SoftDeletableManagerMixin(object): Manager that limits the queryset by default to show only not removed instances of model. """ - use_for_related_fields = True _queryset_class = SoftDeletableQuerySet def get_queryset(self): From 336ac14144a509102e1e86a28cc8115c21a5433a Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Thu, 5 Jan 2017 18:32:33 +0000 Subject: [PATCH 123/271] Add changes at the top of the Changelog --- CHANGES.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 421c6bf..07ebff8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,11 @@ master (unreleased) ------------------- * Fix infinite recursion with multiple `MonitorField` and `defer()` or `only()` -on Django 1.10+. Thanks Romain Garrigues. Merge of #242, fixes #241. + on Django 1.10+. Thanks Romain Garrigues. Merge of #242, fixes #241. +* Fix issue #249 when extending `Manager` and `QuerySet` classes to add custom + methods. +* Add mixins for `SoftDeletableQuerySet` and `SoftDeletableManager`, as stated + in the the documentation. 2.6 (2016.09.19) From ecf80381dee2ff4e963b6f5dec7701638a54f186 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Sat, 7 Jan 2017 14:20:00 +0000 Subject: [PATCH 124/271] Reword changes, use GH- convention for tickets and PR --- CHANGES.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 07ebff8..62388ae 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,9 +5,10 @@ master (unreleased) ------------------- * Fix infinite recursion with multiple `MonitorField` and `defer()` or `only()` - on Django 1.10+. Thanks Romain Garrigues. Merge of #242, fixes #241. -* Fix issue #249 when extending `Manager` and `QuerySet` classes to add custom - methods. + on Django 1.10+. Thanks Romain Garrigues. Merge of GH-242, fixes GH-241. +* Fix `InheritanceManager` and `SoftDeletableManager` to respect ` + `self._queryset_class` instead of hardcoding the queryset class. Merge of + GH-250, fixes GH-249. * Add mixins for `SoftDeletableQuerySet` and `SoftDeletableManager`, as stated in the the documentation. From abd163f2973e706d78b3d0f6a1561573f00fa103 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Sun, 8 Jan 2017 11:18:31 +0000 Subject: [PATCH 125/271] Remove extra backtick --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 62388ae..689813e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,7 @@ master (unreleased) * Fix infinite recursion with multiple `MonitorField` and `defer()` or `only()` on Django 1.10+. Thanks Romain Garrigues. Merge of GH-242, fixes GH-241. -* Fix `InheritanceManager` and `SoftDeletableManager` to respect ` +* Fix `InheritanceManager` and `SoftDeletableManager` to respect `self._queryset_class` instead of hardcoding the queryset class. Merge of GH-250, fixes GH-249. * Add mixins for `SoftDeletableQuerySet` and `SoftDeletableManager`, as stated From 0787a098fd3f419f769440ec81a611973cf91959 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Mon, 9 Jan 2017 13:29:07 +0000 Subject: [PATCH 126/271] Update changelog about 2 recent pull requests https://github.com/carljm/django-model-utils/pull/239 https://github.com/carljm/django-model-utils/pull/240 --- CHANGES.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 689813e..99e0164 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,10 @@ master (unreleased) GH-250, fixes GH-249. * Add mixins for `SoftDeletableQuerySet` and `SoftDeletableManager`, as stated in the the documentation. +* Fix `SoftDeletableModel.delete()` to use the correct database connection. + Merge of GH-239. +* Added boolean keyword argument `soft` to `SoftDeletableModel.delete()` that + revert to default behavior when set to `False`. Merge of GH-240. 2.6 (2016.09.19) From 679af01f26acdb786fdeff8c45aa5abb857c673c Mon Sep 17 00:00:00 2001 From: Romain G Date: Mon, 9 Jan 2017 19:02:59 +0000 Subject: [PATCH 127/271] Add fix + tests for abstract manager inheritance issue with StatusModel --- model_utils/models.py | 9 +++++++ model_utils/tests/models.py | 23 ++++++++++++++++ .../tests/test_models/test_status_model.py | 26 ++++++++++++++++++- 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/model_utils/models.py b/model_utils/models.py index 9c022cb..c679fc6 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -64,6 +64,11 @@ def add_status_query_managers(sender, **kwargs): """ if not issubclass(sender, StatusModel): return + + if django.VERSION >= (1, 10): + # First, get current manager name... + default_manager = sender._meta.default_manager + for value, display in getattr(sender, 'STATUS', ()): if _field_exists(sender, value): raise ImproperlyConfigured( @@ -73,6 +78,10 @@ def add_status_query_managers(sender, **kwargs): ) sender.add_to_class(value, QueryManager(status=value)) + if django.VERSION >= (1, 10): + # ...then, put it back, as add_to_class is modifying the default manager! + sender._meta.default_manager_name = default_manager.name + def add_timeframed_query_manager(sender, **kwargs): """ diff --git a/model_utils/tests/models.py b/model_utils/tests/models.py index c10468d..900e139 100644 --- a/model_utils/tests/models.py +++ b/model_utils/tests/models.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from django.db import models +from django.db.models import QuerySet from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ @@ -125,6 +126,28 @@ class StatusManagerAdded(StatusModel): ) +class StatusCustomQuerySet(QuerySet): + def active_or_deleted(self): + statuses = ['active', 'deleted'] + return self.filter(status__in=statuses) + + +class AbstractStatusCustomManager(StatusModel): + STATUS = Choices( + ("first_choice", _("First choice")), + ("second_choice", _("Second choice")), + ) + + objects = StatusCustomQuerySet.as_manager() + + class Meta: + abstract = True + + +class StatusCustomManager(AbstractStatusCustomManager): + title = models.CharField(max_length=50) + + class Post(models.Model): published = models.BooleanField(default=False) confirmed = models.BooleanField(default=False) diff --git a/model_utils/tests/test_models/test_status_model.py b/model_utils/tests/test_models/test_status_model.py index 995b4c6..6ab7e9b 100644 --- a/model_utils/tests/test_models/test_status_model.py +++ b/model_utils/tests/test_models/test_status_model.py @@ -4,7 +4,7 @@ from freezegun import freeze_time from django.test.testcases import TestCase -from model_utils.tests.models import Status, StatusPlainTuple +from model_utils.tests.models import Status, StatusPlainTuple, StatusCustomManager class StatusModelTests(TestCase): @@ -44,3 +44,27 @@ class StatusModelPlainTupleTests(StatusModelTests): self.model = StatusPlainTuple self.on_hold = StatusPlainTuple.STATUS[2][0] self.active = StatusPlainTuple.STATUS[0][0] + + +class StatusModelDefaultManagerTests(TestCase): + + def test_default_manager_is_not_status_model_generated_ones(self): + # Regression test for https://github.com/carljm/django-model-utils/issues/251 + # The logic behind order for managers seems to have changed in Django 1.10 + # and affects default manager. + # This code was previously failing because the first custom manager (which filters + # with first Choice value, here 'first_choice') generated by StatusModel was + # considered as default manager... + # This situation only happens when we define a model inheriting from an "abstract" + # class which defines an "objects" manager. + + StatusCustomManager.objects.create(status='first_choice') + StatusCustomManager.objects.create(status='second_choice') + StatusCustomManager.objects.create(status='second_choice') + + # ...which made this count() equal to 1 (only 1 element with status='first_choice')... + self.assertEqual(StatusCustomManager._default_manager.count(), 3) + + # ...and this one equal to 0, because of 2 successive filters of 'first_choice' + # (default manager) and 'second_choice' (explicit filter below). + self.assertEqual(StatusCustomManager._default_manager.filter(status='second_choice').count(), 2) From 63e5c59c66e988868dff40bf8f8503ce3e0b7e4c Mon Sep 17 00:00:00 2001 From: Romain G Date: Mon, 9 Jan 2017 19:16:09 +0000 Subject: [PATCH 128/271] Simplify test models + make it compatible for old Django versions --- model_utils/tests/models.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/model_utils/tests/models.py b/model_utils/tests/models.py index 900e139..7bc8012 100644 --- a/model_utils/tests/models.py +++ b/model_utils/tests/models.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from django.db import models -from django.db.models import QuerySet +from django.db.models import Manager from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ @@ -126,10 +126,8 @@ class StatusManagerAdded(StatusModel): ) -class StatusCustomQuerySet(QuerySet): - def active_or_deleted(self): - statuses = ['active', 'deleted'] - return self.filter(status__in=statuses) +class StatusCustomManager(Manager): + pass class AbstractStatusCustomManager(StatusModel): @@ -138,7 +136,7 @@ class AbstractStatusCustomManager(StatusModel): ("second_choice", _("Second choice")), ) - objects = StatusCustomQuerySet.as_manager() + objects = StatusCustomManager() class Meta: abstract = True From 7ae505733ec1c7096e4241b1b80423bb4f864a00 Mon Sep 17 00:00:00 2001 From: Romain G Date: Wed, 11 Jan 2017 12:21:42 +0000 Subject: [PATCH 129/271] Update CHANGES --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 99e0164..bd1a034 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,7 +15,9 @@ master (unreleased) Merge of GH-239. * Added boolean keyword argument `soft` to `SoftDeletableModel.delete()` that revert to default behavior when set to `False`. Merge of GH-240. - +* Enforced default manager in `StatusModel` to avoid manager order issues + when using abstract models that redefine `objects` manager. Merge of GH-253, + fixes GH-251. 2.6 (2016.09.19) ---------------- From 03fd2fb0a05bb82731c7b83414f54044fb9e4002 Mon Sep 17 00:00:00 2001 From: Romain G Date: Wed, 11 Jan 2017 12:22:59 +0000 Subject: [PATCH 130/271] Add missing newline --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index bd1a034..fe88873 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -19,6 +19,7 @@ master (unreleased) when using abstract models that redefine `objects` manager. Merge of GH-253, fixes GH-251. + 2.6 (2016.09.19) ---------------- From 6a01f3389ae4ccba2e3cde86c55eefe4409236cb Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 11 Jan 2017 15:35:51 -0800 Subject: [PATCH 131/271] Add spaces in changelog for consistency with older sections. --- CHANGES.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index fe88873..6606da1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,18 +6,23 @@ master (unreleased) * Fix infinite recursion with multiple `MonitorField` and `defer()` or `only()` on Django 1.10+. Thanks Romain Garrigues. Merge of GH-242, fixes GH-241. + * Fix `InheritanceManager` and `SoftDeletableManager` to respect `self._queryset_class` instead of hardcoding the queryset class. Merge of GH-250, fixes GH-249. + * Add mixins for `SoftDeletableQuerySet` and `SoftDeletableManager`, as stated in the the documentation. + * Fix `SoftDeletableModel.delete()` to use the correct database connection. Merge of GH-239. + * Added boolean keyword argument `soft` to `SoftDeletableModel.delete()` that revert to default behavior when set to `False`. Merge of GH-240. -* Enforced default manager in `StatusModel` to avoid manager order issues - when using abstract models that redefine `objects` manager. Merge of GH-253, - fixes GH-251. + +* Enforced default manager in `StatusModel` to avoid manager order issues when + using abstract models that redefine `objects` manager. Merge of GH-253, fixes + GH-251. 2.6 (2016.09.19) From 269f6f130f2af00c393a971d970ac713dfcc61e9 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 11 Jan 2017 15:37:06 -0800 Subject: [PATCH 132/271] Bump version to 2.6.1. --- CHANGES.rst | 4 ++-- model_utils/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6606da1..902c4c2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,8 +1,8 @@ CHANGES ======= -master (unreleased) -------------------- +2.6.1 (2017.01.11) +------------------ * Fix infinite recursion with multiple `MonitorField` and `defer()` or `only()` on Django 1.10+. Thanks Romain Garrigues. Merge of GH-242, fixes GH-241. diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 0df8970..318a042 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.6.1.a1' +__version__ = '2.6.1' From 625e1041cce85dfc5d1514be7d344502fdf6c076 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 11 Jan 2017 16:07:17 -0800 Subject: [PATCH 133/271] Bump version to 2.6.2.a1. --- CHANGES.rst | 3 +++ model_utils/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 902c4c2..fe1498a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,9 @@ CHANGES ======= +master (unreleased) +------------------- + 2.6.1 (2017.01.11) ------------------ diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 318a042..0f6b5ea 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.6.1' +__version__ = '2.6.2.a1' From fda2d39ec48002464b89bff319d2f2247a565034 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Sat, 26 Mar 2016 20:02:49 -0400 Subject: [PATCH 134/271] Drop unsupported django versions --- .travis.yml | 18 +----- CHANGES.rst | 5 ++ README.rst | 5 +- model_utils/fields.py | 27 -------- model_utils/managers.py | 8 +-- model_utils/tests/helpers.py | 5 -- model_utils/tests/test_fields/__init__.py | 5 -- .../tests/test_fields/test_field_tracker.py | 41 ++++++------ model_utils/tests/test_managers/__init__.py | 5 -- .../test_managers/test_inheritance_manager.py | 62 +++---------------- model_utils/tests/test_miscellaneous.py | 31 ---------- model_utils/tests/test_models/__init__.py | 5 -- model_utils/tests/tests.py | 9 --- setup.py | 4 +- tox.ini | 13 +--- 15 files changed, 44 insertions(+), 199 deletions(-) delete mode 100644 model_utils/tests/helpers.py delete mode 100644 model_utils/tests/tests.py diff --git a/.travis.yml b/.travis.yml index c38cff6..2729137 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,30 +3,18 @@ language: python python: 2.7 env: - - TOXENV=py26-django14 - - TOXENV=py26-django15 - - TOXENV=py26-django16 - - TOXENV=py27-django110 - - TOXENV=py27-django14 - - TOXENV=py27-django15 - - TOXENV=py27-django15_nosouth - - TOXENV=py27-django16 - - TOXENV=py27-django17 - TOXENV=py27-django18 - TOXENV=py27-django19 + - TOXENV=py27-django110 - TOXENV=py27-django_trunk - - TOXENV=py33-django15 - - TOXENV=py33-django16 - - TOXENV=py33-django17 - TOXENV=py33-django18 - - TOXENV=py34-django110 - - TOXENV=py34-django17 - TOXENV=py34-django18 - TOXENV=py34-django19 + - TOXENV=py34-django110 - TOXENV=py34-django_trunk - - TOXENV=py35-django110 - TOXENV=py35-django18 - TOXENV=py35-django19 + - TOXENV=py35-django110 - TOXENV=py35-django_trunk install: diff --git a/CHANGES.rst b/CHANGES.rst index fe1498a..bc23926 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,11 @@ CHANGES master (unreleased) ------------------- +* Drop support for Python 2.6. + +* Drop support for Django 1.4, 1.5, 1.6, 1.7. + + 2.6.1 (2017.01.11) ------------------ diff --git a/README.rst b/README.rst index 001712f..9ce6788 100644 --- a/README.rst +++ b/README.rst @@ -11,9 +11,8 @@ django-model-utils Django model mixins and utilities. -``django-model-utils`` supports `Django`_ 1.4 through 1.9 (latest bugfix -release in each series only) on Python 2.6 (through Django 1.6 only), 2.7, 3.3 -(through Django 1.8 only), 3.4 and 3.5. +``django-model-utils`` supports `Django`_ 1.8 through 1.10 (latest bugfix +release in each series only) on Python 2.7, 3.3 (Django 1.8 only), 3.4 and 3.5. .. _Django: http://www.djangoproject.com/ diff --git a/model_utils/fields.py b/model_utils/fields.py index fe9d4a8..a349b7c 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -241,30 +241,3 @@ class SplitField(models.TextField): name, path, args, kwargs = super(SplitField, self).deconstruct() kwargs['no_excerpt_field'] = True return name, path, args, kwargs - -# allow South to handle these fields smoothly -try: - from south.modelsinspector import add_introspection_rules - # For a normal MarkupField, the add_excerpt_field attribute is - # always True, which means no_excerpt_field arg will always be - # True in a frozen MarkupField, which is what we want. - add_introspection_rules(rules=[ - ( - (SplitField,), - [], - {'no_excerpt_field': ('add_excerpt_field', {})} - ), - ( - (MonitorField,), - [], - {'monitor': ('monitor', {})} - ), - ( - (StatusField,), - [], - {'no_check_for_status': ('check_for_status', {})} - ), - ], patterns=['model_utils\.fields\.']) -except ImportError: - pass - diff --git a/model_utils/managers.py b/model_utils/managers.py index 0b677ff..3e345c9 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -5,12 +5,8 @@ from django.db.models.fields.related import OneToOneField, OneToOneRel from django.db.models.query import QuerySet from django.core.exceptions import ObjectDoesNotExist -try: - from django.db.models.constants import LOOKUP_SEP - from django.utils.six import string_types -except ImportError: # Django < 1.5 - from django.db.models.sql.constants import LOOKUP_SEP - string_types = (basestring,) +from django.db.models.constants import LOOKUP_SEP +from django.utils.six import string_types class InheritanceQuerySetMixin(object): diff --git a/model_utils/tests/helpers.py b/model_utils/tests/helpers.py deleted file mode 100644 index 499dd52..0000000 --- a/model_utils/tests/helpers.py +++ /dev/null @@ -1,5 +0,0 @@ - -try: - from unittest import skipUnless -except ImportError: # Python 2.6 - from django.utils.unittest import skipUnless diff --git a/model_utils/tests/test_fields/__init__.py b/model_utils/tests/test_fields/__init__.py index 23a349f..e69de29 100644 --- a/model_utils/tests/test_fields/__init__.py +++ b/model_utils/tests/test_fields/__init__.py @@ -1,5 +0,0 @@ -# Needed for Django 1.4/1.5 test runner -from .test_field_tracker import * -from .test_monitor_field import * -from .test_split_field import * -from .test_status_field import * diff --git a/model_utils/tests/test_fields/test_field_tracker.py b/model_utils/tests/test_fields/test_field_tracker.py index c215f77..8d6641c 100644 --- a/model_utils/tests/test_fields/test_field_tracker.py +++ b/model_utils/tests/test_fields/test_field_tracker.py @@ -1,11 +1,12 @@ from __future__ import unicode_literals +from unittest import skipUnless + import django from django.core.exceptions import FieldError from django.test import TestCase from model_utils import FieldTracker -from model_utils.tests.helpers import skipUnless from model_utils.tests.models import ( Tracked, TrackedFK, InheritedTrackedFK, TrackedNotDefault, TrackedNonFieldAttr, TrackedMultiple, InheritedTracked, TrackedFileField, @@ -97,15 +98,14 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): self.assertPrevious(name=None, number=None, mutable=None) self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) self.assertChanged(name=None, number=None, mutable=None) - # Django 1.4 doesn't have update_fields - if django.VERSION >= (1, 5, 0): - self.instance.save(update_fields=[]) - self.assertHasChanged(name=True, number=True, mutable=True) - self.assertPrevious(name=None, number=None, mutable=None) - self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) - self.assertChanged(name=None, number=None, mutable=None) - with self.assertRaises(ValueError): - self.instance.save(update_fields=['number']) + + self.instance.save(update_fields=[]) + self.assertHasChanged(name=True, number=True, mutable=True) + self.assertPrevious(name=None, number=None, mutable=None) + self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) + self.assertChanged(name=None, number=None, mutable=None) + with self.assertRaises(ValueError): + self.instance.save(update_fields=['number']) def test_post_save_has_changed(self): self.update_instance(name='retro', number=4, mutable=[1,2,3]) @@ -153,8 +153,6 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): self.instance.save() self.assertCurrent(id=self.instance.id, name='new age', number=8, mutable=[1,4,3]) - @skipUnless( - django.VERSION >= (1, 5, 0), "Django 1.4 doesn't have update_fields") def test_update_fields(self): self.update_instance(name='retro', number=4, mutable=[1,2,3]) self.assertChanged() @@ -280,8 +278,6 @@ class FieldTrackedModelCustomTests(FieldTrackerTestCase, self.instance.save() self.assertCurrent(name='new age') - @skipUnless( - django.VERSION >= (1, 5, 0), "Django 1.4 doesn't have update_fields") def test_update_fields(self): self.update_instance(name='retro', number=4) self.assertChanged() @@ -622,15 +618,14 @@ class ModelTrackerTests(FieldTrackerTests): self.assertPrevious(name=None, number=None, mutable=None) self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) self.assertChanged() - # Django 1.4 doesn't have update_fields - if django.VERSION >= (1, 5, 0): - self.instance.save(update_fields=[]) - self.assertHasChanged(name=True, number=True, mutable=True) - self.assertPrevious(name=None, number=None, mutable=None) - self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) - self.assertChanged() - with self.assertRaises(ValueError): - self.instance.save(update_fields=['number']) + + self.instance.save(update_fields=[]) + self.assertHasChanged(name=True, number=True, mutable=True) + self.assertPrevious(name=None, number=None, mutable=None) + self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) + self.assertChanged() + with self.assertRaises(ValueError): + self.instance.save(update_fields=['number']) def test_pre_save_has_changed(self): self.assertHasChanged(name=True, number=True) diff --git a/model_utils/tests/test_managers/__init__.py b/model_utils/tests/test_managers/__init__.py index 92661aa..e69de29 100644 --- a/model_utils/tests/test_managers/__init__.py +++ b/model_utils/tests/test_managers/__init__.py @@ -1,5 +0,0 @@ -# Needed for Django 1.4/1.5 test runner -from .test_inheritance_manager import * -from .test_query_manager import * -from .test_status_manager import * -from .test_softdelete_manager import * diff --git a/model_utils/tests/test_managers/test_inheritance_manager.py b/model_utils/tests/test_managers/test_inheritance_manager.py index 65d8b59..0c2b91d 100644 --- a/model_utils/tests/test_managers/test_inheritance_manager.py +++ b/model_utils/tests/test_managers/test_inheritance_manager.py @@ -1,10 +1,11 @@ from __future__ import unicode_literals +from unittest import skipUnless + import django from django.db import models from django.test import TestCase -from model_utils.tests.helpers import skipUnless from model_utils.tests.models import (InheritanceManagerTestRelated, InheritanceManagerTestGrandChild1, InheritanceManagerTestGrandChild1_2, InheritanceManagerTestParent, InheritanceManagerTestChild1, @@ -34,12 +35,8 @@ class InheritanceManagerTests(TestCase): def test_select_all_subclasses(self): children = set([self.child1, self.child2]) - if django.VERSION >= (1, 6, 0): - children.add(self.grandchild1) - children.add(self.grandchild1_2) - else: - children.add(InheritanceManagerTestChild1(pk=self.grandchild1.pk)) - children.add(InheritanceManagerTestChild1(pk=self.grandchild1_2.pk)) + children.add(self.grandchild1) + children.add(self.grandchild1_2) self.assertEqual( set(self.get_manager().select_subclasses()), children) @@ -68,7 +65,6 @@ class InheritanceManagerTests(TestCase): children, ) - @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") def test_select_specific_grandchildren(self): children = set([ InheritanceManagerTestParent(pk=self.child1.pk), @@ -85,7 +81,6 @@ class InheritanceManagerTests(TestCase): children, ) - @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") def test_children_and_grandchildren(self): children = set([ self.child1, @@ -120,39 +115,9 @@ class InheritanceManagerTests(TestCase): "inheritancemanagertestchild2").get(pk=self.child1.pk) obj.inheritancemanagertestchild1 - @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") def test_version_determining_any_depth(self): self.assertIsNone(self.get_manager().all()._get_maximum_depth()) - @skipUnless(django.VERSION < (1, 6, 0), "test only applies to Django < 1.6") - def test_version_determining_only_child_depth(self): - self.assertEqual(1, self.get_manager().all()._get_maximum_depth()) - - @skipUnless(django.VERSION < (1, 6, 0), "test only applies to Django < 1.6") - def test_manually_specifying_parent_fk_only_children(self): - """ - given a Model which inherits from another Model, but also declares - the OneToOne link manually using `related_name` and `parent_link`, - ensure that the relation names and subclasses are obtained correctly. - """ - child3 = InheritanceManagerTestChild3.objects.create() - results = InheritanceManagerTestParent.objects.all().select_subclasses() - - expected_objs = [self.child1, self.child2, - InheritanceManagerTestChild1(pk=self.grandchild1.pk), - InheritanceManagerTestChild1(pk=self.grandchild1_2.pk), - child3] - self.assertEqual(list(results), expected_objs) - - expected_related_names = [ - 'inheritancemanagertestchild1', - 'inheritancemanagertestchild2', - 'manual_onetoone', # this was set via parent_link & related_name - ] - self.assertEqual(set(results.subclasses), - set(expected_related_names)) - - @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") def test_manually_specifying_parent_fk_including_grandchildren(self): """ given a Model which inherits from another Model, but also declares @@ -267,7 +232,6 @@ class InheritanceManagerUsingModelsTests(TestCase): self.assertEqual(objs.subclasses, objsmodels.subclasses) self.assertEqual(list(objs), list(objsmodels)) - @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") def test_select_subclass_by_grandchild_model(self): """ Confirm that passing a grandchild model works the same as passing the @@ -281,7 +245,6 @@ class InheritanceManagerUsingModelsTests(TestCase): self.assertEqual(objs.subclasses, objsmodels.subclasses) self.assertEqual(list(objs), list(objsmodels)) - @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") def test_selecting_all_subclasses_specifically_grandchildren(self): """ A bare select_subclasses() should achieve the same results as doing @@ -310,16 +273,11 @@ class InheritanceManagerUsingModelsTests(TestCase): """ objs = InheritanceManagerTestParent.objects.select_subclasses().order_by('pk') - if django.VERSION >= (1, 6, 0): - models = (InheritanceManagerTestChild1, - InheritanceManagerTestChild2, - InheritanceManagerTestChild3, - InheritanceManagerTestGrandChild1, - InheritanceManagerTestGrandChild1_2) - else: - models = (InheritanceManagerTestChild1, - InheritanceManagerTestChild2, - InheritanceManagerTestChild3) + models = (InheritanceManagerTestChild1, + InheritanceManagerTestChild2, + InheritanceManagerTestChild3, + InheritanceManagerTestGrandChild1, + InheritanceManagerTestGrandChild1_2) objsmodels = InheritanceManagerTestParent.objects.select_subclasses( *models).order_by('pk') @@ -353,7 +311,6 @@ class InheritanceManagerUsingModelsTests(TestCase): InheritanceManagerTestParent.objects.select_subclasses( TimeFrame).order_by('pk') - @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") def test_mixing_strings_and_classes_with_grandchildren(self): """ Given arguments consisting of both strings and model classes, @@ -414,7 +371,6 @@ class InheritanceManagerUsingModelsTests(TestCase): InheritanceManagerTestParent(pk=self.grandchild1_2.pk), ]) - @skipUnless(django.VERSION >= (1, 6, 0), "test only applies to Django 1.6+") def test_child_doesnt_accidentally_get_parent(self): """ Given a Child model which also has an InheritanceManager, diff --git a/model_utils/tests/test_miscellaneous.py b/model_utils/tests/test_miscellaneous.py index 8b1df05..2f34fbb 100644 --- a/model_utils/tests/test_miscellaneous.py +++ b/model_utils/tests/test_miscellaneous.py @@ -1,20 +1,12 @@ from __future__ import unicode_literals -import django -from django.db.models.fields import FieldDoesNotExist from django.core.management import call_command from django.test import TestCase from model_utils.fields import get_excerpt -from model_utils.tests.models import ( - Article, - StatusFieldDefaultFilled, -) -from model_utils.tests.helpers import skipUnless class MigrationsTests(TestCase): - @skipUnless(django.VERSION >= (1, 7, 0), "test only applies to Django 1.7+") def test_makemigrations(self): call_command('makemigrations', dry_run=True) @@ -35,26 +27,3 @@ class GetExcerptTests(TestCase): def test_middle_of_line(self): e = get_excerpt("some text more text") self.assertEqual(e, "some text more text") - -try: - from south.modelsinspector import introspector -except ImportError: - introspector = None - - -@skipUnless(introspector, 'South is not installed') -class SouthFreezingTests(TestCase): - def test_introspector_adds_no_excerpt_field(self): - mf = Article._meta.get_field('body') - args, kwargs = introspector(mf) - self.assertEqual(kwargs['no_excerpt_field'], 'True') - - def test_no_excerpt_field_works(self): - from .models import NoRendered - with self.assertRaises(FieldDoesNotExist): - NoRendered._meta.get_field('_body_excerpt') - - def test_status_field_no_check_for_status(self): - sf = StatusFieldDefaultFilled._meta.get_field('status') - args, kwargs = introspector(sf) - self.assertEqual(kwargs['no_check_for_status'], 'True') diff --git a/model_utils/tests/test_models/__init__.py b/model_utils/tests/test_models/__init__.py index 6a0970b..e69de29 100644 --- a/model_utils/tests/test_models/__init__.py +++ b/model_utils/tests/test_models/__init__.py @@ -1,5 +0,0 @@ -# Needed for Django 1.4/1.5 test runner -from .test_softdeletable_model import * -from .test_status_model import * -from .test_timeframed_model import * -from .test_timestamped_model import * diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py deleted file mode 100644 index b023bad..0000000 --- a/model_utils/tests/tests.py +++ /dev/null @@ -1,9 +0,0 @@ -import django - -# Needed for Django 1.4/1.5 test runner -if django.VERSION < (1, 6): - from .test_fields import * - from .test_managers import * - from .test_models import * - from .test_choices import * - from .test_miscellaneous import * diff --git a/setup.py b/setup.py index d894b58..5a4e330 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ setup( author_email='carl@oddbird.net', url='https://github.com/carljm/django-model-utils/', packages=find_packages(), - install_requires=['Django>=1.4.2'], + install_requires=['Django>=1.8'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', @@ -47,7 +47,7 @@ setup( 'Framework :: Django', ], zip_safe=False, - tests_require=["Django>=1.4.2"], + tests_require=['Django>=1.8'], test_suite='runtests.runtests', package_data={ 'model_utils': [ diff --git a/tox.ini b/tox.ini index 6ce334e..089f211 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,12 @@ [tox] envlist = - py26-django{14,15,16}, - py27-django{14,19,110,_trunk}, py27-django15_nosouth, - py{27,33}-django{15,16,17,18}, - py34-django{17,18,19,110,_trunk}, + py27-django{18,19,110,_trunk}, + py33-django{18}, + py34-django{18,19,110,_trunk}, py35-django{18,19,110,_trunk}, [testenv] basepython = - py26: python2.6 py27: python2.7 py33: python3.3 py34: python3.4 @@ -16,15 +14,10 @@ basepython = deps = coverage == 3.6 - django14: Django>=1.4,<1.5 - django15{,_nosouth}: Django>=1.5,<1.6 - django16: Django>=1.6,<1.7 - django17: Django>=1.7,<1.8 django18: Django>=1.8,<1.9 django19: Django>=1.9,<1.10 django110: Django>=1.10,<1.11 django_trunk: https://github.com/django/django/tarball/master - django{14,15,16}: South==1.0.2 freezegun == 0.3.8 commands = coverage run -a setup.py test From cd9e296f81baae9dbf434e4959a4c2ccd36969ee Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Sat, 26 Mar 2016 20:27:20 -0400 Subject: [PATCH 135/271] Update docs --- docs/managers.rst | 7 ------- docs/setup.rst | 4 ++-- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/docs/managers.rst b/docs/managers.rst index 9a626d2..d5b5fb0 100644 --- a/docs/managers.rst +++ b/docs/managers.rst @@ -84,14 +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. -.. note:: - - Due to `Django bug #16572`_, on Django versions prior to 1.6 - ``InheritanceManager`` only supports a single level of model inheritance; - it won't work for grandchild models. - .. _contributed by Jeff Elmore: http://jeffelmore.org/2010/11/11/automatic-downcasting-of-inherited-models-in-django/ -.. _Django bug #16572: https://code.djangoproject.com/ticket/16572 .. _QueryManager: diff --git a/docs/setup.rst b/docs/setup.rst index 5621649..db2a34e 100644 --- a/docs/setup.rst +++ b/docs/setup.rst @@ -17,7 +17,7 @@ modify your ``INSTALLED_APPS`` setting. Dependencies ============ -``django-model-utils`` supports `Django`_ 1.4.2 and later on Python 2.6, 2.7, -3.2, and 3.3. +``django-model-utils`` supports `Django`_ 1.8 through 1.10 (latest bugfix +release in each series only) on Python 2.7, 3.3 (Django 1.8 only), 3.4 and 3.5. .. _Django: http://www.djangoproject.com/ From e148c670d102e547d7d5a925c6f44a1925ee10d8 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Sat, 26 Mar 2016 20:43:30 -0400 Subject: [PATCH 136/271] Fix django 2.0 warnings --- model_utils/fields.py | 2 +- model_utils/managers.py | 6 +++--- model_utils/tests/models.py | 13 ++++++++----- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/model_utils/fields.py b/model_utils/fields.py index a349b7c..f308706 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -228,7 +228,7 @@ class SplitField(models.TextField): return value.content def value_to_string(self, obj): - value = self._get_val_from_obj(obj) + value = self.value_from_object(obj) return value.content def get_prep_value(self, value): diff --git a/model_utils/managers.py b/model_utils/managers.py index 3e345c9..6558bf9 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -139,10 +139,10 @@ class InheritanceQuerySetMixin(object): if levels: levels -= 1 while parent_link is not None: - if django.VERSION < (1, 8): - related = parent_link.related - else: + if django.VERSION < (1, 9): related = parent_link.rel + else: + related = parent_link.remote_field ancestry.insert(0, related.get_accessor_name()) if levels or levels is None: if django.VERSION < (1, 8): diff --git a/model_utils/tests/models.py b/model_utils/tests/models.py index 7bc8012..8043c9f 100644 --- a/model_utils/tests/models.py +++ b/model_utils/tests/models.py @@ -28,9 +28,12 @@ class InheritanceManagerTestParent(models.Model): # FileField is just a handy descriptor-using field. Refs #6. non_related_field_using_descriptor = models.FileField(upload_to="test") related = models.ForeignKey( - InheritanceManagerTestRelated, related_name="imtests", null=True) + InheritanceManagerTestRelated, related_name="imtests", null=True, + on_delete=models.CASCADE) normal_field = models.TextField() - related_self = models.OneToOneField("self", related_name="imtests_self", null=True) + related_self = models.OneToOneField( + "self", related_name="imtests_self", null=True, + on_delete=models.CASCADE) objects = InheritanceManager() def __unicode__(self): @@ -65,7 +68,7 @@ class InheritanceManagerTestChild2(InheritanceManagerTestParent): class InheritanceManagerTestChild3(InheritanceManagerTestParent): parent_ptr = models.OneToOneField( InheritanceManagerTestParent, related_name='manual_onetoone', - parent_link=True) + parent_link=True, on_delete=models.CASCADE) class TimeStamp(TimeStampedModel): @@ -219,7 +222,7 @@ class Tracked(models.Model): class TrackedFK(models.Model): - fk = models.ForeignKey('Tracked') + fk = models.ForeignKey('Tracked', on_delete=models.CASCADE) tracker = FieldTracker() custom_tracker = FieldTracker(fields=['fk_id']) @@ -275,7 +278,7 @@ class ModelTracked(models.Model): class ModelTrackedFK(models.Model): - fk = models.ForeignKey('ModelTracked') + fk = models.ForeignKey('ModelTracked', on_delete=models.CASCADE) tracker = ModelTracker() custom_tracker = ModelTracker(fields=['fk_id']) From d07b992b940dfae69fc8bf5e91a354ee4f8c11b9 Mon Sep 17 00:00:00 2001 From: Romain G Date: Thu, 12 Jan 2017 18:34:47 +0000 Subject: [PATCH 137/271] Drop old get_query_set syntax, replaced by get_queryset now --- model_utils/managers.py | 11 +---------- model_utils/tests/models.py | 2 -- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/model_utils/managers.py b/model_utils/managers.py index 6558bf9..6ec9301 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -199,8 +199,6 @@ class InheritanceManagerMixin(object): def get_queryset(self): return self._queryset_class(self.model) - get_query_set = get_queryset - def select_subclasses(self, *subclasses): return self.get_queryset().select_subclasses(*subclasses) @@ -228,16 +226,11 @@ class QueryManagerMixin(object): return self def get_queryset(self): - try: - qs = super(QueryManagerMixin, self).get_queryset().filter(self._q) - except AttributeError: - qs = super(QueryManagerMixin, self).get_query_set().filter(self._q) + qs = super(QueryManagerMixin, self).get_queryset().filter(self._q) if self._order_by is not None: return qs.order_by(*self._order_by) return qs - get_query_set = get_queryset - class QueryManager(QueryManagerMixin, models.Manager): pass @@ -278,8 +271,6 @@ class SoftDeletableManagerMixin(object): return self._queryset_class(**kwargs).filter(is_removed=False) - get_query_set = get_queryset - class SoftDeletableManager(SoftDeletableManagerMixin, models.Manager): pass diff --git a/model_utils/tests/models.py b/model_utils/tests/models.py index 8043c9f..db13413 100644 --- a/model_utils/tests/models.py +++ b/model_utils/tests/models.py @@ -210,8 +210,6 @@ class FeaturedManager(models.Manager): kwargs["using"] = self._db return ByAuthorQuerySet(self.model, **kwargs).filter(feature=True) - get_query_set = get_queryset - class Tracked(models.Model): name = models.CharField(max_length=20) From 2ee8627c5ad3346ac3c93d5723789edf48c7dfc5 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Tue, 17 Jan 2017 22:34:04 +0000 Subject: [PATCH 138/271] Pull request and issue template fixes #254 --- .github/ISSUE_TEMPLATE.md | 14 ++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 15 +++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..f384dde --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,14 @@ +## Problem + +Explain the problem you encountered. + +## Environment + +- Django Model Utils version: +- Django version: +- Python version: +- Other libraries used, if any: + +## Code examples + +Give code example that demonstrates the issue, or even better, write new tests that fails because of that issue. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..63db703 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +## Problem + +Explain the problem you are fixing (add the link to the related issue(s), if any). + +## Solution + +Explain the solution that has been implemented, and what has been changed. + +## Commandments + +- [ ] Write PEP8 compliant code. +- [ ] Cover it with tests. +- [ ] Update `CHANGES.rst` file to describe the changes, and quote according issue with `GH-`. +- [ ] Pay attention to backward compatibility, or if it breaks it, explain why. +- [ ] Update documentation (if relevant). From 7d1f2b5be58aabc5dab99f0f2be572ad8d76d4a3 Mon Sep 17 00:00:00 2001 From: romgar Date: Sat, 21 Jan 2017 14:02:31 +0000 Subject: [PATCH 139/271] Remove compatibility code in runtests.py file --- runtests.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/runtests.py b/runtests.py index a1f4d93..b49d38c 100755 --- a/runtests.py +++ b/runtests.py @@ -24,21 +24,14 @@ def runtests(): if not settings.configured: settings.configure(**DEFAULT_SETTINGS) - # Compatibility with Django 1.7's stricter initialization - if hasattr(django, 'setup'): - django.setup() + django.setup() parent = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, parent) - try: - from django.test.runner import DiscoverRunner - runner_class = DiscoverRunner - test_args = ['model_utils.tests'] - except ImportError: - from django.test.simple import DjangoTestSuiteRunner - runner_class = DjangoTestSuiteRunner - test_args = ['tests'] + from django.test.runner import DiscoverRunner + runner_class = DiscoverRunner + test_args = ['model_utils.tests'] failures = runner_class( verbosity=1, interactive=True, failfast=False).run_tests(test_args) From ced4afe8f91d92420c0f8e974f9eaadc7c722401 Mon Sep 17 00:00:00 2001 From: romgar Date: Wed, 18 Jan 2017 18:40:53 +0000 Subject: [PATCH 140/271] Update documentation to remove references of PassThroughManager, removed in version 2.4 --- docs/managers.rst | 39 +++++++++------------------------------ 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/docs/managers.rst b/docs/managers.rst index d5b5fb0..d9a9654 100644 --- a/docs/managers.rst +++ b/docs/managers.rst @@ -117,15 +117,6 @@ set the ordering of the ``QuerySet`` returned by the ``QueryManager`` by chaining a call to ``.order_by()`` on the ``QueryManager`` (this is not required). - -PassThroughManager ------------------- - -`PassThroughManager` was removed in django-model-utils 2.4. Use Django's -built-in `QuerySet.as_manager()` and/or `Manager.from_queryset()` utilities -instead. - - SoftDeletableManager -------------------- @@ -138,7 +129,7 @@ Mixins Each of the above manager classes has a corresponding mixin that can be used to add functionality to any manager. For example, to create a GeoDjango -``GeoManager`` that includes "pass through" functionality, you can write the +``GeoManager`` that includes "soft deletable" functionality, you can write the following code: .. code-block:: python @@ -146,36 +137,24 @@ following code: from django.contrib.gis.db import models from django.contrib.gis.db.models.query import GeoQuerySet - from model_utils.managers import PassThroughManagerMixin + from model_utils.managers import SoftDeletableManagerMixin - class PassThroughGeoManager(PassThroughManagerMixin, models.GeoManager): + class SoftDeletableGeoManager(SoftDeletableManagerMixin, models.GeoManager): pass - class LocationQuerySet(GeoQuerySet): - def within_boundary(self, geom): - return self.filter(point__within=geom) - - def public(self): - return self.filter(public=True) - - class Location(models.Model): - point = models.PointField() + class Location(SoftDeletableModel): public = models.BooleanField(default=True) - objects = PassThroughGeoManager.for_queryset_class(LocationQuerySet)() + objects = SoftDeletableGeoManager() - Location.objects.public() - Location.objects.within_boundary(geom=geom) - Location.objects.within_boundary(geom=geom).public() + # This delete will be a "soft" delete + Location.objects.delete() -Now you have a "pass through manager" that can also take advantage of +Now you have a "soft delete manager" that can also take advantage of GeoDjango's spatial lookups. You can similarly add additional functionality to any manager by composing that manager with ``InheritanceManagerMixin`` or ``QueryManagerMixin``. (Note that any manager class using ``InheritanceManagerMixin`` must return a ``QuerySet`` class using ``InheritanceQuerySetMixin`` from its ``get_queryset`` -method. This means that if composing ``InheritanceManagerMixin`` and -``PassThroughManagerMixin``, the ``QuerySet`` class passed to -``PassThroughManager.for_queryset_class`` must inherit -``InheritanceQuerySetMixin``.) +method.) From 9abcf0c943999654a5cbf4e81abd41353e9443f6 Mon Sep 17 00:00:00 2001 From: Alexander Kavanaugh Date: Wed, 25 Jan 2017 19:36:59 -0800 Subject: [PATCH 141/271] Add "license" arg to setup() call This helps for seeing at-a-glance licenses in services such as pyup.io --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 5a4e330..65b7641 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ def get_version(root_path): setup( name='django-model-utils', version=get_version(HERE), + license="BSD", description='Django model mixins and utilities', long_description=long_description, author='Carl Meyer', From 7552c4a7f25aeac4dfac953848f325325a4acd3f Mon Sep 17 00:00:00 2001 From: romgar Date: Thu, 26 Jan 2017 14:18:19 +0000 Subject: [PATCH 142/271] Remove out-to-date example --- docs/managers.rst | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/docs/managers.rst b/docs/managers.rst index d9a9654..43aa030 100644 --- a/docs/managers.rst +++ b/docs/managers.rst @@ -128,33 +128,8 @@ Mixins ------ Each of the above manager classes has a corresponding mixin that can be used to -add functionality to any manager. For example, to create a GeoDjango -``GeoManager`` that includes "soft deletable" functionality, you can write the -following code: +add functionality to any manager. -.. code-block:: python - - from django.contrib.gis.db import models - from django.contrib.gis.db.models.query import GeoQuerySet - - from model_utils.managers import SoftDeletableManagerMixin - - class SoftDeletableGeoManager(SoftDeletableManagerMixin, models.GeoManager): - pass - - class Location(SoftDeletableModel): - public = models.BooleanField(default=True) - objects = SoftDeletableGeoManager() - - # This delete will be a "soft" delete - Location.objects.delete() - - -Now you have a "soft delete manager" that can also take advantage of -GeoDjango's spatial lookups. You can similarly add additional functionality to -any manager by composing that manager with ``InheritanceManagerMixin`` or -``QueryManagerMixin``. - -(Note that any manager class using ``InheritanceManagerMixin`` must return a +Note that any manager class using ``InheritanceManagerMixin`` must return a ``QuerySet`` class using ``InheritanceQuerySetMixin`` from its ``get_queryset`` -method.) +method. From 10ebc1627148e3d80bfddc7554dc99a0fde2f681 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Wed, 15 Feb 2017 23:00:10 +0000 Subject: [PATCH 143/271] Exclude tests from distribution fixes #258 --- runtests.py | 13 ++++++------- setup.py | 4 +--- {model_utils/tests => tests}/__init__.py | 0 {model_utils/tests => tests}/fields.py | 0 {model_utils/tests => tests}/managers.py | 0 {model_utils/tests => tests}/models.py | 6 +++--- {model_utils/tests => tests}/test_choices.py | 0 .../tests => tests}/test_fields/__init__.py | 0 .../test_fields/test_field_tracker.py | 2 +- .../test_fields/test_monitor_field.py | 2 +- .../tests => tests}/test_fields/test_split_field.py | 2 +- .../test_fields/test_status_field.py | 2 +- .../tests => tests}/test_managers/__init__.py | 0 .../test_managers/test_inheritance_manager.py | 2 +- .../test_managers/test_query_manager.py | 2 +- .../test_managers/test_softdelete_manager.py | 2 +- .../test_managers/test_status_manager.py | 2 +- {model_utils/tests => tests}/test_miscellaneous.py | 0 .../tests => tests}/test_models/__init__.py | 0 .../test_models/test_softdeletable_model.py | 2 +- .../test_models/test_status_model.py | 2 +- .../test_models/test_timeframed_model.py | 2 +- .../test_models/test_timestamped_model.py | 2 +- tox.ini | 2 +- translations.py | 12 +++++------- 25 files changed, 28 insertions(+), 33 deletions(-) rename {model_utils/tests => tests}/__init__.py (100%) rename {model_utils/tests => tests}/fields.py (100%) rename {model_utils/tests => tests}/managers.py (100%) rename {model_utils/tests => tests}/models.py (98%) rename {model_utils/tests => tests}/test_choices.py (100%) rename {model_utils/tests => tests}/test_fields/__init__.py (100%) rename {model_utils/tests => tests}/test_fields/test_field_tracker.py (99%) rename {model_utils/tests => tests}/test_fields/test_monitor_field.py (97%) rename {model_utils/tests => tests}/test_fields/test_split_field.py (97%) rename {model_utils/tests => tests}/test_fields/test_status_field.py (96%) rename {model_utils/tests => tests}/test_managers/__init__.py (100%) rename {model_utils/tests => tests}/test_managers/test_inheritance_manager.py (99%) rename {model_utils/tests => tests}/test_managers/test_query_manager.py (95%) rename {model_utils/tests => tests}/test_managers/test_softdelete_manager.py (94%) rename {model_utils/tests => tests}/test_managers/test_status_manager.py (92%) rename {model_utils/tests => tests}/test_miscellaneous.py (100%) rename {model_utils/tests => tests}/test_models/__init__.py (100%) rename {model_utils/tests => tests}/test_models/test_softdeletable_model.py (97%) rename {model_utils/tests => tests}/test_models/test_status_model.py (97%) rename {model_utils/tests => tests}/test_models/test_timeframed_model.py (95%) rename {model_utils/tests => tests}/test_models/test_timestamped_model.py (92%) diff --git a/runtests.py b/runtests.py index b49d38c..74c8619 100755 --- a/runtests.py +++ b/runtests.py @@ -5,19 +5,18 @@ import os, sys from django.conf import settings import django - DEFAULT_SETTINGS = dict( INSTALLED_APPS=( 'model_utils', - 'model_utils.tests', - ), + 'tests', + ), DATABASES={ "default": { "ENGINE": "django.db.backends.sqlite3" - } - }, + } + }, SILENCED_SYSTEM_CHECKS=["1_7.W001"], - ) +) def runtests(): @@ -31,7 +30,7 @@ def runtests(): from django.test.runner import DiscoverRunner runner_class = DiscoverRunner - test_args = ['model_utils.tests'] + test_args = ['tests'] failures = runner_class( verbosity=1, interactive=True, failfast=False).run_tests(test_args) diff --git a/setup.py b/setup.py index 65b7641..04c4d95 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ setup( author='Carl Meyer', author_email='carl@oddbird.net', url='https://github.com/carljm/django-model-utils/', - packages=find_packages(), + packages=find_packages(exclude=['tests*']), install_requires=['Django>=1.8'], classifiers=[ 'Development Status :: 5 - Production/Stable', @@ -39,7 +39,6 @@ setup( 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', @@ -49,7 +48,6 @@ setup( ], zip_safe=False, tests_require=['Django>=1.8'], - test_suite='runtests.runtests', package_data={ 'model_utils': [ 'locale/*/LC_MESSAGES/django.po','locale/*/LC_MESSAGES/django.mo' diff --git a/model_utils/tests/__init__.py b/tests/__init__.py similarity index 100% rename from model_utils/tests/__init__.py rename to tests/__init__.py diff --git a/model_utils/tests/fields.py b/tests/fields.py similarity index 100% rename from model_utils/tests/fields.py rename to tests/fields.py diff --git a/model_utils/tests/managers.py b/tests/managers.py similarity index 100% rename from model_utils/tests/managers.py rename to tests/managers.py diff --git a/model_utils/tests/models.py b/tests/models.py similarity index 98% rename from model_utils/tests/models.py rename to tests/models.py index db13413..a65d499 100644 --- a/model_utils/tests/models.py +++ b/tests/models.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals +from __future__ import unicode_literals, absolute_import from django.db import models from django.db.models import Manager @@ -14,8 +14,8 @@ from model_utils.models import ( TimeFramedModel, TimeStampedModel, ) -from model_utils.tests.fields import MutableField -from model_utils.tests.managers import CustomSoftDeleteManager +from tests.fields import MutableField +from tests.managers import CustomSoftDeleteManager from model_utils.tracker import FieldTracker, ModelTracker diff --git a/model_utils/tests/test_choices.py b/tests/test_choices.py similarity index 100% rename from model_utils/tests/test_choices.py rename to tests/test_choices.py diff --git a/model_utils/tests/test_fields/__init__.py b/tests/test_fields/__init__.py similarity index 100% rename from model_utils/tests/test_fields/__init__.py rename to tests/test_fields/__init__.py diff --git a/model_utils/tests/test_fields/test_field_tracker.py b/tests/test_fields/test_field_tracker.py similarity index 99% rename from model_utils/tests/test_fields/test_field_tracker.py rename to tests/test_fields/test_field_tracker.py index 8d6641c..00c28b3 100644 --- a/model_utils/tests/test_fields/test_field_tracker.py +++ b/tests/test_fields/test_field_tracker.py @@ -7,7 +7,7 @@ from django.core.exceptions import FieldError from django.test import TestCase from model_utils import FieldTracker -from model_utils.tests.models import ( +from tests.models import ( Tracked, TrackedFK, InheritedTrackedFK, TrackedNotDefault, TrackedNonFieldAttr, TrackedMultiple, InheritedTracked, TrackedFileField, ModelTracked, ModelTrackedFK, ModelTrackedNotDefault, ModelTrackedMultiple, InheritedModelTracked, diff --git a/model_utils/tests/test_fields/test_monitor_field.py b/tests/test_fields/test_monitor_field.py similarity index 97% rename from model_utils/tests/test_fields/test_monitor_field.py rename to tests/test_fields/test_monitor_field.py index 779f502..6c5792e 100644 --- a/model_utils/tests/test_fields/test_monitor_field.py +++ b/tests/test_fields/test_monitor_field.py @@ -7,7 +7,7 @@ from freezegun import freeze_time from django.test import TestCase from model_utils.fields import MonitorField -from model_utils.tests.models import Monitored, MonitorWhen, MonitorWhenEmpty, DoubleMonitored +from tests.models import Monitored, MonitorWhen, MonitorWhenEmpty, DoubleMonitored class MonitorFieldTests(TestCase): diff --git a/model_utils/tests/test_fields/test_split_field.py b/tests/test_fields/test_split_field.py similarity index 97% rename from model_utils/tests/test_fields/test_split_field.py rename to tests/test_fields/test_split_field.py index 57802fd..dfde85f 100644 --- a/model_utils/tests/test_fields/test_split_field.py +++ b/tests/test_fields/test_split_field.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.utils.six import text_type from django.test import TestCase -from model_utils.tests.models import Article, SplitFieldAbstractParent +from tests.models import Article, SplitFieldAbstractParent class SplitFieldTests(TestCase): diff --git a/model_utils/tests/test_fields/test_status_field.py b/tests/test_fields/test_status_field.py similarity index 96% rename from model_utils/tests/test_fields/test_status_field.py rename to tests/test_fields/test_status_field.py index 73dabac..5f077da 100644 --- a/model_utils/tests/test_fields/test_status_field.py +++ b/tests/test_fields/test_status_field.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.test import TestCase from model_utils.fields import StatusField -from model_utils.tests.models import ( +from tests.models import ( Article, StatusFieldDefaultFilled, StatusFieldDefaultNotFilled, StatusFieldChoicesName, ) diff --git a/model_utils/tests/test_managers/__init__.py b/tests/test_managers/__init__.py similarity index 100% rename from model_utils/tests/test_managers/__init__.py rename to tests/test_managers/__init__.py diff --git a/model_utils/tests/test_managers/test_inheritance_manager.py b/tests/test_managers/test_inheritance_manager.py similarity index 99% rename from model_utils/tests/test_managers/test_inheritance_manager.py rename to tests/test_managers/test_inheritance_manager.py index 0c2b91d..4509175 100644 --- a/model_utils/tests/test_managers/test_inheritance_manager.py +++ b/tests/test_managers/test_inheritance_manager.py @@ -6,7 +6,7 @@ import django from django.db import models from django.test import TestCase -from model_utils.tests.models import (InheritanceManagerTestRelated, InheritanceManagerTestGrandChild1, +from tests.models import (InheritanceManagerTestRelated, InheritanceManagerTestGrandChild1, InheritanceManagerTestGrandChild1_2, InheritanceManagerTestParent, InheritanceManagerTestChild1, InheritanceManagerTestChild2, TimeFrame, InheritanceManagerTestChild3 diff --git a/model_utils/tests/test_managers/test_query_manager.py b/tests/test_managers/test_query_manager.py similarity index 95% rename from model_utils/tests/test_managers/test_query_manager.py rename to tests/test_managers/test_query_manager.py index 70f2f46..dd539b6 100644 --- a/model_utils/tests/test_managers/test_query_manager.py +++ b/tests/test_managers/test_query_manager.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.test import TestCase -from model_utils.tests.models import Post +from tests.models import Post class QueryManagerTests(TestCase): diff --git a/model_utils/tests/test_managers/test_softdelete_manager.py b/tests/test_managers/test_softdelete_manager.py similarity index 94% rename from model_utils/tests/test_managers/test_softdelete_manager.py rename to tests/test_managers/test_softdelete_manager.py index 3f5ed47..4ae5475 100644 --- a/model_utils/tests/test_managers/test_softdelete_manager.py +++ b/tests/test_managers/test_softdelete_manager.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.test import TestCase -from model_utils.tests.models import CustomSoftDelete +from tests.models import CustomSoftDelete class CustomSoftDeleteManagerTests(TestCase): diff --git a/model_utils/tests/test_managers/test_status_manager.py b/tests/test_managers/test_status_manager.py similarity index 92% rename from model_utils/tests/test_managers/test_status_manager.py rename to tests/test_managers/test_status_manager.py index af3d7cb..593a547 100644 --- a/model_utils/tests/test_managers/test_status_manager.py +++ b/tests/test_managers/test_status_manager.py @@ -6,7 +6,7 @@ from django.test import TestCase from model_utils.managers import QueryManager from model_utils.models import StatusModel -from model_utils.tests.models import StatusManagerAdded +from tests.models import StatusManagerAdded class StatusManagerAddedTests(TestCase): diff --git a/model_utils/tests/test_miscellaneous.py b/tests/test_miscellaneous.py similarity index 100% rename from model_utils/tests/test_miscellaneous.py rename to tests/test_miscellaneous.py diff --git a/model_utils/tests/test_models/__init__.py b/tests/test_models/__init__.py similarity index 100% rename from model_utils/tests/test_models/__init__.py rename to tests/test_models/__init__.py diff --git a/model_utils/tests/test_models/test_softdeletable_model.py b/tests/test_models/test_softdeletable_model.py similarity index 97% rename from model_utils/tests/test_models/test_softdeletable_model.py rename to tests/test_models/test_softdeletable_model.py index fb05e08..5f06fd3 100644 --- a/model_utils/tests/test_models/test_softdeletable_model.py +++ b/tests/test_models/test_softdeletable_model.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.db.utils import ConnectionDoesNotExist from django.test import TestCase -from model_utils.tests.models import SoftDeletable +from tests.models import SoftDeletable class SoftDeletableModelTests(TestCase): diff --git a/model_utils/tests/test_models/test_status_model.py b/tests/test_models/test_status_model.py similarity index 97% rename from model_utils/tests/test_models/test_status_model.py rename to tests/test_models/test_status_model.py index 6ab7e9b..724ba26 100644 --- a/model_utils/tests/test_models/test_status_model.py +++ b/tests/test_models/test_status_model.py @@ -4,7 +4,7 @@ from freezegun import freeze_time from django.test.testcases import TestCase -from model_utils.tests.models import Status, StatusPlainTuple, StatusCustomManager +from tests.models import Status, StatusPlainTuple, StatusCustomManager class StatusModelTests(TestCase): diff --git a/model_utils/tests/test_models/test_timeframed_model.py b/tests/test_models/test_timeframed_model.py similarity index 95% rename from model_utils/tests/test_models/test_timeframed_model.py rename to tests/test_models/test_timeframed_model.py index 993a339..dccc5a7 100644 --- a/model_utils/tests/test_models/test_timeframed_model.py +++ b/tests/test_models/test_timeframed_model.py @@ -8,7 +8,7 @@ from django.test import TestCase from model_utils.managers import QueryManager from model_utils.models import TimeFramedModel -from model_utils.tests.models import TimeFrame, TimeFrameManagerAdded +from tests.models import TimeFrame, TimeFrameManagerAdded class TimeFramedModelTests(TestCase): diff --git a/model_utils/tests/test_models/test_timestamped_model.py b/tests/test_models/test_timestamped_model.py similarity index 92% rename from model_utils/tests/test_models/test_timestamped_model.py rename to tests/test_models/test_timestamped_model.py index 221d682..8760411 100644 --- a/model_utils/tests/test_models/test_timestamped_model.py +++ b/tests/test_models/test_timestamped_model.py @@ -6,7 +6,7 @@ from freezegun import freeze_time from django.test import TestCase -from model_utils.tests.models import TimeStamp +from tests.models import TimeStamp class TimeStampedModelTests(TestCase): diff --git a/tox.ini b/tox.ini index 089f211..3705195 100644 --- a/tox.ini +++ b/tox.ini @@ -20,4 +20,4 @@ deps = django_trunk: https://github.com/django/django/tarball/master freezegun == 0.3.8 -commands = coverage run -a setup.py test +commands = coverage run runtests.py diff --git a/translations.py b/translations.py index 58b107f..8eebf95 100755 --- a/translations.py +++ b/translations.py @@ -1,24 +1,22 @@ #!/usr/bin/env python - import os import sys from django.conf import settings import django - DEFAULT_SETTINGS = dict( INSTALLED_APPS=( 'model_utils', - 'model_utils.tests', - ), + 'tests', + ), DATABASES={ "default": { "ENGINE": "django.db.backends.sqlite3" - } - }, + } + }, SILENCED_SYSTEM_CHECKS=["1_7.W001"], - ) +) def run(command): From d6edfdb4db747d0fc461c12ced472962f19efc8d Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Wed, 15 Feb 2017 23:13:55 +0000 Subject: [PATCH 144/271] Update CHANGES.rst --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index bc23926..88fa89e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,8 +5,8 @@ master (unreleased) ------------------- * Drop support for Python 2.6. - * Drop support for Django 1.4, 1.5, 1.6, 1.7. +* Exclude tests from the distribution, fixes GH-258. 2.6.1 (2017.01.11) From 1473707fc726d85f377790c5d718c65e706cb555 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Thu, 16 Feb 2017 12:39:41 +0000 Subject: [PATCH 145/271] Run coverage with '-a' option --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3705195..79b5c1d 100644 --- a/tox.ini +++ b/tox.ini @@ -20,4 +20,4 @@ deps = django_trunk: https://github.com/django/django/tarball/master freezegun == 0.3.8 -commands = coverage run runtests.py +commands = coverage run -a runtests.py From 8cb21aabbcba6aac389b2c073024fea08878126e Mon Sep 17 00:00:00 2001 From: Sachi King Date: Sun, 2 Apr 2017 21:34:55 +1000 Subject: [PATCH 146/271] Support django 1.11 iterator changes Django starting with 1.9 switched to using a class to provide an iterator for the querymanager. Between 1.9 and 1.10 changes slowly stopped referencing that function and instead started calling _iterator_class directly. As the functionality model-utils is patching has moved, this patch moves the iterator logic to a class to match the changes that have been made in Django in version 1.9. As Django 1.8 is a LTS release that is still supported, iterator() is retained in the InheritanceQuerySetMixin and can be removed when support for Django 1.8 is removed. This goes for the try-except in the import statements as well. --- CHANGES.rst | 1 + model_utils/managers.py | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 88fa89e..2d99060 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,7 @@ master (unreleased) * Drop support for Python 2.6. * Drop support for Django 1.4, 1.5, 1.6, 1.7. * Exclude tests from the distribution, fixes GH-258. +* Add support for Django 1.11 GH-269 2.6.1 (2017.01.11) diff --git a/model_utils/managers.py b/model_utils/managers.py index 6ec9301..9dc68a2 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -3,13 +3,54 @@ import django from django.db import models from django.db.models.fields.related import OneToOneField, OneToOneRel from django.db.models.query import QuerySet +try: + from django.db.models.query import BaseIterable, ModelIterable +except ImportError: + # Django 1.8 does not have iterable classes + BaseIterable = object from django.core.exceptions import ObjectDoesNotExist from django.db.models.constants import LOOKUP_SEP from django.utils.six import string_types +class InheritanceIterable(BaseIterable): + def __iter__(self): + queryset = self.queryset + iter = ModelIterable(queryset) + if getattr(queryset, 'subclasses', False): + extras = tuple(queryset.query.extra.keys()) + # sort the subclass names longest first, + # so with 'a' and 'a__b' it goes as deep as possible + subclasses = sorted(queryset.subclasses, key=len, reverse=True) + for obj in iter: + sub_obj = None + for s in subclasses: + sub_obj = queryset._get_sub_obj_recurse(obj, s) + if sub_obj: + break + if not sub_obj: + sub_obj = obj + + if getattr(queryset, '_annotated', False): + for k in queryset._annotated: + setattr(sub_obj, k, getattr(obj, k)) + + for k in extras: + setattr(sub_obj, k, getattr(obj, k)) + + yield sub_obj + else: + for obj in iter: + yield obj + + class InheritanceQuerySetMixin(object): + def __init__(self, *args, **kwargs): + super(InheritanceQuerySetMixin, self).__init__(*args, **kwargs) + if django.VERSION > (1, 8): + self._iterable_class = InheritanceIterable + def select_subclasses(self, *subclasses): levels = self._get_maximum_depth() calculated_subclasses = self._get_subclasses_recurse( @@ -64,6 +105,7 @@ class InheritanceQuerySetMixin(object): return qset def iterator(self): + # Maintained for Django 1.8 compatability iter = super(InheritanceQuerySetMixin, self).iterator() if getattr(self, 'subclasses', False): extras = tuple(self.query.extra.keys()) From 89c4a0fc441cbda9a000f152039d3b57cb56e03c Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 13 Apr 2017 16:39:21 -0700 Subject: [PATCH 147/271] Add testing on Python 3.6 and Django 1.11. --- .travis.yml | 10 ++++++---- tox.ini | 9 ++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2729137..9862fb4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,16 +6,19 @@ env: - TOXENV=py27-django18 - TOXENV=py27-django19 - TOXENV=py27-django110 - - TOXENV=py27-django_trunk + - TOXENV=py27-django111 - TOXENV=py33-django18 - TOXENV=py34-django18 - TOXENV=py34-django19 - TOXENV=py34-django110 - - TOXENV=py34-django_trunk + - TOXENV=py34-django111 - TOXENV=py35-django18 - TOXENV=py35-django19 - TOXENV=py35-django110 + - TOXENV=py35-django111 - TOXENV=py35-django_trunk + - TOXENV=py36-django111 + - TOXENV=py36-django_trunk install: - pip install --upgrade pip setuptools tox virtualenv coveralls @@ -25,8 +28,7 @@ script: matrix: allow_failures: - - env: TOXENV=py27-django_trunk - - env: TOXENV=py34-django_trunk - env: TOXENV=py35-django_trunk + - env: TOXENV=py36-django_trunk after_success: coveralls diff --git a/tox.ini b/tox.ini index 79b5c1d..cf3ff01 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,10 @@ [tox] envlist = - py27-django{18,19,110,_trunk}, + py27-django{18,19,110,111}, py33-django{18}, - py34-django{18,19,110,_trunk}, - py35-django{18,19,110,_trunk}, + py34-django{18,19,110,111}, + py35-django{18,19,110,111,_trunk}, + py36-django{111,_trunk}, [testenv] basepython = @@ -11,12 +12,14 @@ basepython = py33: python3.3 py34: python3.4 py35: python3.5 + py36: python3.6 deps = coverage == 3.6 django18: Django>=1.8,<1.9 django19: Django>=1.9,<1.10 django110: Django>=1.10,<1.11 + django111: Django>=1.11,<1.12 django_trunk: https://github.com/django/django/tarball/master freezegun == 0.3.8 From f11646b10ea6918e70ec9114c9f7f2bd668e6ab2 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 13 Apr 2017 16:47:48 -0700 Subject: [PATCH 148/271] Allow failure of py36 build for now, until Travis gets it --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 9862fb4..5e01979 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,6 +29,7 @@ script: matrix: allow_failures: - env: TOXENV=py35-django_trunk + - env: TOXENV=py36-django111 - env: TOXENV=py36-django_trunk after_success: coveralls From 2e3b50dcb67f8d0231dd82c494721e965419f0ff Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 13 Apr 2017 16:49:58 -0700 Subject: [PATCH 149/271] Bump version to 3.0.0. --- CHANGES.rst | 4 ++-- model_utils/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2d99060..efa85a9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,8 +1,8 @@ CHANGES ======= -master (unreleased) -------------------- +3.0.0 (2017.04.13) +------------------ * Drop support for Python 2.6. * Drop support for Django 1.4, 1.5, 1.6, 1.7. diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 0f6b5ea..54c239c 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '2.6.2.a1' +__version__ = '3.0.0' From 100d2d7a24873b36c6800c19a50f837211e174df Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 13 Apr 2017 16:51:52 -0700 Subject: [PATCH 150/271] Bump version to 3.0.1a1. --- CHANGES.rst | 3 +++ model_utils/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index efa85a9..a578c65 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,9 @@ CHANGES ======= +master (unreleased) +------------------- + 3.0.0 (2017.04.13) ------------------ diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 54c239c..fceae3a 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '3.0.0' +__version__ = '3.0.1a1' From 198dcb612eeb6fa5eaec875b65d060a7e343286c Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sat, 15 Apr 2017 08:47:31 -0700 Subject: [PATCH 151/271] Update github links for jazzband transfer. --- README.rst | 14 +++++++------- docs/index.rst | 4 ++-- setup.py | 3 ++- tests/test_models/test_status_model.py | 2 +- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index 9ce6788..4700438 100644 --- a/README.rst +++ b/README.rst @@ -2,10 +2,10 @@ django-model-utils ================== -.. image:: https://secure.travis-ci.org/carljm/django-model-utils.png?branch=master - :target: http://travis-ci.org/carljm/django-model-utils -.. image:: https://coveralls.io/repos/carljm/django-model-utils/badge.png?branch=master - :target: https://coveralls.io/r/carljm/django-model-utils +.. image:: https://secure.travis-ci.org/jazzband/django-model-utils.png?branch=master + :target: http://travis-ci.org/jazzband/django-model-utils +.. image:: https://coveralls.io/repos/jazzband/django-model-utils/badge.png?branch=master + :target: https://coveralls.io/r/jazzband/django-model-utils .. image:: https://img.shields.io/pypi/v/django-model-utils.svg :target: https://crate.io/packages/django-model-utils @@ -33,6 +33,6 @@ Contributing Please file bugs and send pull requests to the `GitHub repository`_ and `issue tracker`_. See `CONTRIBUTING.rst`_ for details. -.. _GitHub repository: https://github.com/carljm/django-model-utils/ -.. _issue tracker: https://github.com/carljm/django-model-utils/issues -.. _CONTRIBUTING.rst: https://github.com/carljm/django-model-utils/blob/master/CONTRIBUTING.rst +.. _GitHub repository: https://github.com/jazzband/django-model-utils/ +.. _issue tracker: https://github.com/jazzband/django-model-utils/issues +.. _CONTRIBUTING.rst: https://github.com/jazzband/django-model-utils/blob/master/CONTRIBUTING.rst diff --git a/docs/index.rst b/docs/index.rst index 9b6d2bb..411d2c4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,8 +24,8 @@ Contributing Please file bugs and send pull requests to the `GitHub repository`_ and `issue tracker`_. -.. _GitHub repository: https://github.com/carljm/django-model-utils/ -.. _issue tracker: https://github.com/carljm/django-model-utils/issues +.. _GitHub repository: https://github.com/jazzband/django-model-utils/ +.. _issue tracker: https://github.com/jazzband/django-model-utils/issues diff --git a/setup.py b/setup.py index 04c4d95..a5e5904 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,8 @@ setup( long_description=long_description, author='Carl Meyer', author_email='carl@oddbird.net', - url='https://github.com/carljm/django-model-utils/', + maintainer='JazzBand', + url='https://github.com/jazzband/django-model-utils/', packages=find_packages(exclude=['tests*']), install_requires=['Django>=1.8'], classifiers=[ diff --git a/tests/test_models/test_status_model.py b/tests/test_models/test_status_model.py index 724ba26..f660936 100644 --- a/tests/test_models/test_status_model.py +++ b/tests/test_models/test_status_model.py @@ -49,7 +49,7 @@ class StatusModelPlainTupleTests(StatusModelTests): class StatusModelDefaultManagerTests(TestCase): def test_default_manager_is_not_status_model_generated_ones(self): - # Regression test for https://github.com/carljm/django-model-utils/issues/251 + # Regression test for GH-251 # The logic behind order for managers seems to have changed in Django 1.10 # and affects default manager. # This code was previously failing because the first custom manager (which filters From 94c77d75ef010711bc3e6f71f1b6d243a6bc7f08 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sat, 15 Apr 2017 08:50:55 -0700 Subject: [PATCH 152/271] Add jazzband badge/contributing guidelines. --- CONTRIBUTING.rst | 9 +++++++++ README.rst | 3 +++ 2 files changed, 12 insertions(+) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 10fbcca..23b903b 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,6 +1,15 @@ Contributing ============ +.. image:: https://jazzband.co/static/img/jazzband.svg + :target: https://jazzband.co/ + :alt: Jazzband + +This is a `Jazzband `_ project. By contributing you agree +to abide by the `Contributor Code of Conduct +`_ and follow the `guidelines +`_. + Below is a list of tips for submitting issues and pull requests. Submitting Issues diff --git a/README.rst b/README.rst index 4700438..006304a 100644 --- a/README.rst +++ b/README.rst @@ -2,6 +2,9 @@ django-model-utils ================== +.. image:: https://jazzband.co/static/img/badge.svg + :target: https://jazzband.co/ + :alt: Jazzband .. image:: https://secure.travis-ci.org/jazzband/django-model-utils.png?branch=master :target: http://travis-ci.org/jazzband/django-model-utils .. image:: https://coveralls.io/repos/jazzband/django-model-utils/badge.png?branch=master From 343d45940616b92836011a5257d57e4136a72296 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Tue, 18 Apr 2017 10:51:18 +0200 Subject: [PATCH 153/271] Updated some of the badges. --- README.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 006304a..7885b9d 100644 --- a/README.rst +++ b/README.rst @@ -5,12 +5,12 @@ django-model-utils .. image:: https://jazzband.co/static/img/badge.svg :target: https://jazzband.co/ :alt: Jazzband -.. image:: https://secure.travis-ci.org/jazzband/django-model-utils.png?branch=master - :target: http://travis-ci.org/jazzband/django-model-utils -.. image:: https://coveralls.io/repos/jazzband/django-model-utils/badge.png?branch=master - :target: https://coveralls.io/r/jazzband/django-model-utils +.. image:: https://travis-ci.org/jazzband/django-model-utils.svg?branch=master + :target: https://travis-ci.org/jazzband/django-model-utils +.. image:: https://coveralls.io/repos/github/jazzband/django-model-utils/badge.svg?branch=master + :target: https://coveralls.io/github/jazzband/django-model-utils?branch=master .. image:: https://img.shields.io/pypi/v/django-model-utils.svg - :target: https://crate.io/packages/django-model-utils + :target: https://pypi.python.org/pypi/django-model-utils Django model mixins and utilities. From 69d6c6040af67ab948b757ac683fbebdbcb0e399 Mon Sep 17 00:00:00 2001 From: Arseny Sysolyatin Date: Mon, 22 May 2017 20:23:24 +0300 Subject: [PATCH 154/271] Russian locale added --- model_utils/locale/ru/LC_MESSAGES/django.mo | Bin 0 -> 840 bytes model_utils/locale/ru/LC_MESSAGES/django.po | 43 ++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 model_utils/locale/ru/LC_MESSAGES/django.mo create mode 100644 model_utils/locale/ru/LC_MESSAGES/django.po diff --git a/model_utils/locale/ru/LC_MESSAGES/django.mo b/model_utils/locale/ru/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..8edb2d893946d0bf7f530c0ea34767f99bfed20a GIT binary patch literal 840 zcmZXQ&1(}u7{<3+KUhViR|Us|#ZV{iZc}YH+0@vk7%(QpSkT*=ovvFpvtege4D^x; zdhzQ2(SybMk%9-WvJ2h>uY!MoCl5ZmNg9d+zkTPK_kG@Xc7D#yzE!Z!fXko_&Ve@| zT?Zh|eE=uH7jPDQ1*gC_a2k9E7r+njcSccukM*w;ilSow2k|1}44RxroEyh0h?k&m zAi|dQMCpGE>Fge}i0M3~6Ea&eM`7Gix`E?uc@RR{p-jIr65~KR&us1@*9?Pg){(U4 z&`lN!FW@F|wvh`c{LH73^n9VVST6`Atqa$4=-tQ_v>lj4Z8q9;EgcCTUB_gT7@B^Y zYD?5Gh`wSLZY^kwnubE!V$Z#_|G4gPwbG*X23@a^dkwQ*TX@oVP*v~SLeh3=44urC6?k2#!_?CdBnj*jlQUEA~XouFGr1MWt)%V?X~UEFY@O%g_Gy;iSI ze^bwE>RP}h;~3fR;qH>XkT-k2?eSvzK$s(~N9_%|JgrNww#7nPH|h!5lU@w<30 w9>#}C{1I{(?IBpd5ywPC`<;)F63_VG8r5b, 2017. +msgid "" +msgstr "" +"Project-Id-Version: django-model-utils\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-05-22 19:46+0300\n" +"PO-Revision-Date: 2017-05-22 19:46+0300\n" +"Last-Translator: Arseny Sysolyatin \n" +"Language-Team: \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" +"%100>=11 && n%100<=14)? 2 : 3);\n" + +#: models.py:24 +msgid "created" +msgstr "создано" + +#: models.py:25 +msgid "modified" +msgstr "изменено" + +#: models.py:37 +msgid "start" +msgstr "начало" + +#: models.py:38 +msgid "end" +msgstr "конец" + +#: models.py:53 +msgid "status" +msgstr "статус" + +#: models.py:54 +msgid "status changed" +msgstr "статус изменен" From 0a809df4da3c714f0ba4b327028e96c9dc0e17a5 Mon Sep 17 00:00:00 2001 From: Hanley Date: Fri, 23 Jun 2017 09:51:32 -0400 Subject: [PATCH 155/271] make InheritanceIterable inherit from ModelIterable instead of BaseIterable --- AUTHORS.rst | 1 + model_utils/managers.py | 4 ++-- tests/test_inheritance_iterable.py | 22 ++++++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 tests/test_inheritance_iterable.py diff --git a/AUTHORS.rst b/AUTHORS.rst index 4dd3044..fd350c0 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -15,6 +15,7 @@ Facundo Gaich Felipe Prenholato Filipe Ximenes Gregor Müllegger +Hanley Hansen ivirabyan James Oakley Jannis Leidel diff --git a/model_utils/managers.py b/model_utils/managers.py index 9dc68a2..8e022b4 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -7,14 +7,14 @@ try: from django.db.models.query import BaseIterable, ModelIterable except ImportError: # Django 1.8 does not have iterable classes - BaseIterable = object + BaseIterable, ModelIterable = object, object from django.core.exceptions import ObjectDoesNotExist from django.db.models.constants import LOOKUP_SEP from django.utils.six import string_types -class InheritanceIterable(BaseIterable): +class InheritanceIterable(ModelIterable): def __iter__(self): queryset = self.queryset iter = ModelIterable(queryset) diff --git a/tests/test_inheritance_iterable.py b/tests/test_inheritance_iterable.py new file mode 100644 index 0000000..884b763 --- /dev/null +++ b/tests/test_inheritance_iterable.py @@ -0,0 +1,22 @@ +from __future__ import unicode_literals + +from unittest import skipIf + +import django +from django.test import TestCase +from django.db.models import Prefetch + +from tests.models import InheritanceManagerTestParent, InheritanceManagerTestChild1 + + +class InheritanceIterableTest(TestCase): + @skipIf(django.VERSION[:2] == (1, 10), "Django 1.10 expects ModelIterable not a subclass of it") + def test_prefetch(self): + qs = InheritanceManagerTestChild1.objects.all().prefetch_related( + Prefetch( + 'normal_field', + queryset=InheritanceManagerTestParent.objects.all(), + to_attr='normal_field_prefetched' + ) + ) + self.assertEquals(qs.count(), 0) From 8c935d3a83b5689975b800e69c80baed0ac229d3 Mon Sep 17 00:00:00 2001 From: Hanley Date: Fri, 23 Jun 2017 09:56:51 -0400 Subject: [PATCH 156/271] update changes.rst --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index a578c65..b85dcaf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,8 @@ CHANGES master (unreleased) ------------------- +* Update InheritanceIterable to inherit from + ModelIterable instead of BaseIterable, fixes GH-277. 3.0.0 (2017.04.13) ------------------ From fb67a52d0ae2d0ea45e73db971b97948860eda07 Mon Sep 17 00:00:00 2001 From: Adam Dobrawy Date: Sun, 25 Jun 2017 11:26:00 +0200 Subject: [PATCH 157/271] Add missing classifiers in setup.py --- setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.py b/setup.py index a5e5904..82e85da 100644 --- a/setup.py +++ b/setup.py @@ -46,6 +46,10 @@ setup( 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Framework :: Django', + 'Framework :: Django :: 1.8', + 'Framework :: Django :: 1.9', + 'Framework :: Django :: 1.10', + 'Framework :: Django :: 1.11', ], zip_safe=False, tests_require=['Django>=1.8'], From a07140e77165b6e9becef76c4a8609209ace545d Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Thu, 7 Dec 2017 11:13:39 -0600 Subject: [PATCH 158/271] Use _chain for django 2.0, #295 --- model_utils/managers.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/model_utils/managers.py b/model_utils/managers.py index 9dc68a2..5816979 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -90,13 +90,25 @@ class InheritanceQuerySetMixin(object): new_qs.subclasses = subclasses return new_qs - def _clone(self, klass=None, setup=False, **kwargs): + def _chain(self, **kwargs): for name in ['subclasses', '_annotated']: if hasattr(self, name): kwargs[name] = getattr(self, name) + + return super(InheritanceQuerySetMixin, self)._chain(**kwargs) + + def _clone(self, klass=None, setup=False, **kwargs): + if django.VERSION >= (2, 0): + return super(InheritanceQuerySetMixin, self)._clone() + + for name in ['subclasses', '_annotated']: + if hasattr(self, name): + kwargs[name] = getattr(self, name) + if django.VERSION < (1, 9): kwargs['klass'] = klass kwargs['setup'] = setup + return super(InheritanceQuerySetMixin, self)._clone(**kwargs) def annotate(self, *args, **kwargs): From a975f2f07b0c7443618b5aa81d54d7707699c302 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Thu, 7 Dec 2017 11:24:05 -0600 Subject: [PATCH 159/271] Add django 2.0 to travis and tox #297 --- .travis.yml | 3 +++ tox.ini | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5e01979..11ce3c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,12 +12,15 @@ env: - TOXENV=py34-django19 - TOXENV=py34-django110 - TOXENV=py34-django111 + - TOXENV=py34-django200 - TOXENV=py35-django18 - TOXENV=py35-django19 - TOXENV=py35-django110 - TOXENV=py35-django111 + - TOXENV=py35-django200 - TOXENV=py35-django_trunk - TOXENV=py36-django111 + - TOXENV=py36-django200 - TOXENV=py36-django_trunk install: diff --git a/tox.ini b/tox.ini index cf3ff01..f4845ac 100644 --- a/tox.ini +++ b/tox.ini @@ -2,9 +2,9 @@ envlist = py27-django{18,19,110,111}, py33-django{18}, - py34-django{18,19,110,111}, - py35-django{18,19,110,111,_trunk}, - py36-django{111,_trunk}, + py34-django{18,19,110,111,200}, + py35-django{18,19,110,111,200,_trunk}, + py36-django{111,200,_trunk}, [testenv] basepython = @@ -20,6 +20,7 @@ deps = django19: Django>=1.9,<1.10 django110: Django>=1.10,<1.11 django111: Django>=1.11,<1.12 + django200: Django>=2.0,<2.1 django_trunk: https://github.com/django/django/tarball/master freezegun == 0.3.8 From c4d72123efec77fc5a1e8de53ed20cb72a8fc459 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Thu, 7 Dec 2017 12:20:50 -0600 Subject: [PATCH 160/271] rm use_for_related_fields #290 --- model_utils/managers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/model_utils/managers.py b/model_utils/managers.py index 5816979..e8c5029 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -247,7 +247,6 @@ class InheritanceQuerySet(InheritanceQuerySetMixin, QuerySet): class InheritanceManagerMixin(object): - use_for_related_fields = True _queryset_class = InheritanceQuerySet def get_queryset(self): @@ -265,7 +264,6 @@ class InheritanceManager(InheritanceManagerMixin, models.Manager): class QueryManagerMixin(object): - use_for_related_fields = True def __init__(self, *args, **kwargs): if args: From 35e17150b50e6c69d7c36236d3b9f13dddb88bb4 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Thu, 7 Dec 2017 12:32:46 -0600 Subject: [PATCH 161/271] Overhaul travis/tox testbed #299 Passes in python version via travis python: param --- .travis.yml | 81 +++++++++++++++++++++++++++++++++-------------------- tox.ini | 14 ++------- 2 files changed, 54 insertions(+), 41 deletions(-) diff --git a/.travis.yml b/.travis.yml index 11ce3c6..4e9f4c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,38 +1,59 @@ language: python -python: 2.7 - -env: - - TOXENV=py27-django18 - - TOXENV=py27-django19 - - TOXENV=py27-django110 - - TOXENV=py27-django111 - - TOXENV=py33-django18 - - TOXENV=py34-django18 - - TOXENV=py34-django19 - - TOXENV=py34-django110 - - TOXENV=py34-django111 - - TOXENV=py34-django200 - - TOXENV=py35-django18 - - TOXENV=py35-django19 - - TOXENV=py35-django110 - - TOXENV=py35-django111 - - TOXENV=py35-django200 - - TOXENV=py35-django_trunk - - TOXENV=py36-django111 - - TOXENV=py36-django200 - - TOXENV=py36-django_trunk +matrix: + fast_finish: true + include: + - python: 2.7 + env: TOXENV=py27-django18 + - python: 2.7 + env: TOXENV=py27-django19 + - python: 2.7 + env: TOXENV=py27-django110 + - python: 2.7 + env: TOXENV=py27-django111 + - python: 3.4 + env: TOXENV=py34-django18 + - python: 3.4 + env: TOXENV=py34-django19 + - python: 3.4 + env: TOXENV=py34-django110 + - python: 3.4 + env: TOXENV=py34-django111 + - python: 3.4 + env: TOXENV=py34-django200 + - python: 3.5 + env: TOXENV=py35-django18 + - python: 3.5 + env: TOXENV=py35-django19 + - python: 3.5 + env: TOXENV=py35-django110 + - python: 3.5 + env: TOXENV=py35-django111 + - python: 3.5 + env: TOXENV=py35-django200 + - python: 3.5 + env: TOXENV=py35-djangotrunk + - python: 3.6 + env: TOXENV=py36-django111 + - python: 3.6 + env: TOXENV=py36-django200 + - python: 3.6 + env: TOXENV=py36-djangotrunk + allow_failures: + - python: 3.5 + env: TOXENV=py35-djangotrunk + - python: 3.6 + env: TOXENV=py36-django111 + - python: 3.6 + env: TOXENV=py36-djangotrunk install: - pip install --upgrade pip setuptools tox virtualenv coveralls + - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then export PYVER=py27; fi + - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then export PYVER=py34; fi + - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then export PYVER=py35; fi + - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then export PYVER=py36; fi -script: - - tox - -matrix: - allow_failures: - - env: TOXENV=py35-django_trunk - - env: TOXENV=py36-django111 - - env: TOXENV=py36-django_trunk +script: COMMAND='coverage run' tox -e$TOXENV after_success: coveralls diff --git a/tox.ini b/tox.ini index f4845ac..807e0d4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,19 +1,11 @@ [tox] envlist = py27-django{18,19,110,111}, - py33-django{18}, py34-django{18,19,110,111,200}, - py35-django{18,19,110,111,200,_trunk}, - py36-django{111,200,_trunk}, + py35-django{18,19,110,111,200,trunk}, + py36-django{111,200,trunk}, [testenv] -basepython = - py27: python2.7 - py33: python3.3 - py34: python3.4 - py35: python3.5 - py36: python3.6 - deps = coverage == 3.6 django18: Django>=1.8,<1.9 @@ -21,7 +13,7 @@ deps = django110: Django>=1.10,<1.11 django111: Django>=1.11,<1.12 django200: Django>=2.0,<2.1 - django_trunk: https://github.com/django/django/tarball/master + djangotrunk: https://github.com/django/django/tarball/master freezegun == 0.3.8 commands = coverage run -a runtests.py From 1f8e9f9fb32fd7109b2090ddaa9c57f7739d574e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 11 Dec 2017 07:15:06 -0600 Subject: [PATCH 162/271] rm update travis script, this can be done by hand --- update_travis_envs.sh | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100755 update_travis_envs.sh diff --git a/update_travis_envs.sh b/update_travis_envs.sh deleted file mode 100755 index 8b5d559..0000000 --- a/update_travis_envs.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -# Updates .travis.yml envs based on tox.ini configuration. - -# Removing old environment list -cp ./.travis.yml ./.travis.yml.bak -cat ./.travis.yml.bak | grep -v "^ - TOXENV=" > ./.travis.yml - -# Inserting envs based on list generated by tox -for env_name in $(tox --listenvs | sort -r); do - sed -i "/^env:$/a\ -\ \ - TOXENV=${env_name}" ./.travis.yml; -done From 4a7182112f9210437c7561b5e2af37842467ef50 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 11 Dec 2017 07:20:03 -0600 Subject: [PATCH 163/271] update coveragerc to omit tests, dotfiles --- .coveragerc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.coveragerc b/.coveragerc index 65aaf4d..b802633 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,4 +1,7 @@ [run] source = model_utils omit = model_utils/tests/* + .* + tests/* + */_* branch = 1 From 6dd7556da7e4a4334089553624fd7b297b0c00cb Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 11 Dec 2017 07:24:57 -0600 Subject: [PATCH 164/271] add codecov to travis --- .coveragerc | 4 +--- .travis.yml | 5 +++-- tox.ini | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.coveragerc b/.coveragerc index b802633..62d6d1c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,7 +1,5 @@ [run] source = model_utils -omit = model_utils/tests/* - .* +omit = .* tests/* */_* -branch = 1 diff --git a/.travis.yml b/.travis.yml index 4e9f4c0..ccb420d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: python +sudo: false matrix: fast_finish: true @@ -48,7 +49,7 @@ matrix: env: TOXENV=py36-djangotrunk install: - - pip install --upgrade pip setuptools tox virtualenv coveralls + - pip install --upgrade pip setuptools tox virtualenv codecov - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then export PYVER=py27; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then export PYVER=py34; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then export PYVER=py35; fi @@ -56,4 +57,4 @@ install: script: COMMAND='coverage run' tox -e$TOXENV -after_success: coveralls +after_success: codecov diff --git a/tox.ini b/tox.ini index 807e0d4..91fb3a0 100644 --- a/tox.ini +++ b/tox.ini @@ -16,4 +16,4 @@ deps = djangotrunk: https://github.com/django/django/tarball/master freezegun == 0.3.8 -commands = coverage run -a runtests.py +commands = {env:COMMAND:python} runtests.py From 28bd4567a76a45246904e618b90d2ff09e2cbaef Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 11 Dec 2017 08:08:47 -0600 Subject: [PATCH 165/271] switch to py.test + pytest-cov --- .travis.yml | 2 +- requirements-test.txt | 2 ++ runtests.py | 41 ----------------------------------------- setup.cfg | 4 ++++ tests/settings.py | 10 ++++++++++ tox.ini | 7 +++++-- 6 files changed, 22 insertions(+), 44 deletions(-) create mode 100644 requirements-test.txt delete mode 100755 runtests.py create mode 100644 tests/settings.py diff --git a/.travis.yml b/.travis.yml index ccb420d..fb7ce1d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -55,6 +55,6 @@ install: - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then export PYVER=py35; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then export PYVER=py36; fi -script: COMMAND='coverage run' tox -e$TOXENV +script: tox -e$TOXENV -- --cov # positional args ({posargs}) to pass into tox.ini after_success: codecov diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..493f267 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,2 @@ +pytest==3.3.1 +pytest-django==3.1.2 diff --git a/runtests.py b/runtests.py deleted file mode 100755 index 74c8619..0000000 --- a/runtests.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python - -import os, sys - -from django.conf import settings -import django - -DEFAULT_SETTINGS = dict( - INSTALLED_APPS=( - 'model_utils', - 'tests', - ), - DATABASES={ - "default": { - "ENGINE": "django.db.backends.sqlite3" - } - }, - SILENCED_SYSTEM_CHECKS=["1_7.W001"], -) - - -def runtests(): - if not settings.configured: - settings.configure(**DEFAULT_SETTINGS) - - django.setup() - - parent = os.path.dirname(os.path.abspath(__file__)) - sys.path.insert(0, parent) - - from django.test.runner import DiscoverRunner - runner_class = DiscoverRunner - test_args = ['tests'] - - failures = runner_class( - verbosity=1, interactive=True, failfast=False).run_tests(test_args) - sys.exit(failures) - - -if __name__ == '__main__': - runtests() diff --git a/setup.cfg b/setup.cfg index 7d5a6f7..6058f77 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,5 +3,9 @@ source-dir = docs/ build-dir = docs/_build all_files = 1 +[tool:pytest] +django_find_project = false +DJANGO_SETTINGS_MODULE = tests.settings + [wheel] universal = 1 diff --git a/tests/settings.py b/tests/settings.py new file mode 100644 index 0000000..8817e83 --- /dev/null +++ b/tests/settings.py @@ -0,0 +1,10 @@ +INSTALLED_APPS = ( + 'model_utils', + 'tests', +) +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3' + } +} +SECRET_KEY = 'dummy' diff --git a/tox.ini b/tox.ini index 91fb3a0..83fa7b2 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,6 @@ envlist = [testenv] deps = - coverage == 3.6 django18: Django>=1.8,<1.9 django19: Django>=1.9,<1.10 django110: Django>=1.10,<1.11 @@ -15,5 +14,9 @@ deps = django200: Django>=2.0,<2.1 djangotrunk: https://github.com/django/django/tarball/master freezegun == 0.3.8 + -rrequirements-test.txt + pytest-cov -commands = {env:COMMAND:python} runtests.py +commands = + pip install -e . + py.test {posargs} From 044cac0d68e6c36820a00783f7cd3f03337d2f95 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 11 Dec 2017 08:22:59 -0600 Subject: [PATCH 166/271] pep8 + add django classifiers to setup.py --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a5e5904..7413962 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,7 @@ def get_version(root_path): if line.startswith('__version__ ='): return line.split('=')[1].strip().strip('"\'') + setup( name='django-model-utils', version=get_version(HERE), @@ -46,12 +47,14 @@ setup( 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Framework :: Django', + 'Framework :: Django :: 1.11', + 'Framework :: Django :: 2.0', ], zip_safe=False, tests_require=['Django>=1.8'], package_data={ 'model_utils': [ - 'locale/*/LC_MESSAGES/django.po','locale/*/LC_MESSAGES/django.mo' + 'locale/*/LC_MESSAGES/django.po', 'locale/*/LC_MESSAGES/django.mo' ], }, ) From 8a0d00b93947dd9f6b0717b58a9dbb3a7a386cd4 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 11 Dec 2017 08:27:41 -0600 Subject: [PATCH 167/271] tag 3.1.0 --- CHANGES.rst | 7 +++++++ model_utils/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index a578c65..ac54364 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,13 @@ CHANGES master (unreleased) ------------------- +3.1.0 (2017.12.11) +------------------ + +- Support for Django 2.0 via GH-298, fixes GH-297 +- Remove old travis script via GH-300 +- Fix codecov and switch to py.test #301 + 3.0.0 (2017.04.13) ------------------ diff --git a/model_utils/__init__.py b/model_utils/__init__.py index fceae3a..fbf309a 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '3.0.1a1' +__version__ = '3.1.0' From fc13a6bd40541ac95c56f6c5de7e013c141c9678 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 11 Dec 2017 18:05:28 -0600 Subject: [PATCH 168/271] use codecov button on readme --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 7885b9d..ac45fee 100644 --- a/README.rst +++ b/README.rst @@ -7,8 +7,8 @@ django-model-utils :alt: Jazzband .. image:: https://travis-ci.org/jazzband/django-model-utils.svg?branch=master :target: https://travis-ci.org/jazzband/django-model-utils -.. image:: https://coveralls.io/repos/github/jazzband/django-model-utils/badge.svg?branch=master - :target: https://coveralls.io/github/jazzband/django-model-utils?branch=master +.. image:: https://codecov.io/gh/jazzband/django-model-utils/branch/master/graph/badge.svg + :target: https://codecov.io/gh/jazzband/django-model-utils .. image:: https://img.shields.io/pypi/v/django-model-utils.svg :target: https://pypi.python.org/pypi/django-model-utils From 419fe216b767c6837b67a210b87b5949fbec7b69 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 17 Dec 2017 07:35:34 -0600 Subject: [PATCH 169/271] Simplify version mentioning in README, classifiers Remove Python version mentionings, as they are beholden to Django's constraints. See also: - https://github.com/jazzband/django-model-utils/issues/305 --- README.rst | 8 +++----- setup.py | 4 ++++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index ac45fee..3014eb0 100644 --- a/README.rst +++ b/README.rst @@ -14,8 +14,7 @@ django-model-utils Django model mixins and utilities. -``django-model-utils`` supports `Django`_ 1.8 through 1.10 (latest bugfix -release in each series only) on Python 2.7, 3.3 (Django 1.8 only), 3.4 and 3.5. +``django-model-utils`` supports `Django`_ 1.8 to 2.0. .. _Django: http://www.djangoproject.com/ @@ -23,12 +22,11 @@ This app is available on `PyPI`_. .. _PyPI: https://pypi.python.org/pypi/django-model-utils/ - Getting Help ============ -Documentation for django-model-utils is available at https://django-model-utils.readthedocs.io/ - +Documentation for django-model-utils is available +https://django-model-utils.readthedocs.io/ Contributing ============ diff --git a/setup.py b/setup.py index 7413962..067a64b 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,11 @@ setup( 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Framework :: Django', + 'Framework :: Django :: 1.8', + 'Framework :: Django :: 1.9', + 'Framework :: Django :: 1.10', 'Framework :: Django :: 1.11', 'Framework :: Django :: 2.0', ], From baa8bae1c037580349448fd0ac7c1ddffc0d3a76 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 17 Dec 2017 07:45:56 -0600 Subject: [PATCH 170/271] update changelog and versions for 3.1.1 --- CHANGES.rst | 5 +++++ model_utils/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index ac54364..922821d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,11 @@ CHANGES master (unreleased) ------------------- +3.1.1 (2017.12.17) +------------------ + +- Update classifiers and README via GH-306, fixes GH-305 + 3.1.0 (2017.12.11) ------------------ diff --git a/model_utils/__init__.py b/model_utils/__init__.py index fbf309a..f23bd9c 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '3.1.0' +__version__ = '3.1.1' From ccaa08d12efb27a52e97e661be29cc44c6c2740f Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 10 Jan 2018 10:44:25 +0100 Subject: [PATCH 171/271] Simplified tox setup with tox-travis. --- .travis.yml | 67 +++++++---------------------------------------------- tox.ini | 12 ++++++---- 2 files changed, 16 insertions(+), 63 deletions(-) diff --git a/.travis.yml b/.travis.yml index fb7ce1d..92538f8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,60 +1,11 @@ -language: python sudo: false - -matrix: - fast_finish: true - include: - - python: 2.7 - env: TOXENV=py27-django18 - - python: 2.7 - env: TOXENV=py27-django19 - - python: 2.7 - env: TOXENV=py27-django110 - - python: 2.7 - env: TOXENV=py27-django111 - - python: 3.4 - env: TOXENV=py34-django18 - - python: 3.4 - env: TOXENV=py34-django19 - - python: 3.4 - env: TOXENV=py34-django110 - - python: 3.4 - env: TOXENV=py34-django111 - - python: 3.4 - env: TOXENV=py34-django200 - - python: 3.5 - env: TOXENV=py35-django18 - - python: 3.5 - env: TOXENV=py35-django19 - - python: 3.5 - env: TOXENV=py35-django110 - - python: 3.5 - env: TOXENV=py35-django111 - - python: 3.5 - env: TOXENV=py35-django200 - - python: 3.5 - env: TOXENV=py35-djangotrunk - - python: 3.6 - env: TOXENV=py36-django111 - - python: 3.6 - env: TOXENV=py36-django200 - - python: 3.6 - env: TOXENV=py36-djangotrunk - allow_failures: - - python: 3.5 - env: TOXENV=py35-djangotrunk - - python: 3.6 - env: TOXENV=py36-django111 - - python: 3.6 - env: TOXENV=py36-djangotrunk - -install: - - pip install --upgrade pip setuptools tox virtualenv codecov - - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then export PYVER=py27; fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then export PYVER=py34; fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then export PYVER=py35; fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then export PYVER=py36; fi - -script: tox -e$TOXENV -- --cov # positional args ({posargs}) to pass into tox.ini - +language: python +cache: pip +python: +- 2.7 +- 3.4 +- 3.5 +- 3.6 +install: pip install tox-travis codecov +script: tox -- --cov # positional args ({posargs}) to pass into tox.ini after_success: codecov diff --git a/tox.ini b/tox.ini index 83fa7b2..fe42bb1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,9 @@ [tox] envlist = - py27-django{18,19,110,111}, - py34-django{18,19,110,111,200}, - py35-django{18,19,110,111,200,trunk}, - py36-django{111,200,trunk}, + py27-django{18,19,110,111} + py34-django{18,19,110,111,200} + py35-django{18,19,110,111,200,trunk} + py36-django{111,200,trunk} [testenv] deps = @@ -12,10 +12,12 @@ deps = django110: Django>=1.10,<1.11 django111: Django>=1.11,<1.12 django200: Django>=2.0,<2.1 - djangotrunk: https://github.com/django/django/tarball/master + djangotrunk: https://github.com/django/django/archive/master.tar.gz freezegun == 0.3.8 -rrequirements-test.txt pytest-cov +ignore_outcome = + djangotrunk: True commands = pip install -e . From 1eff6d0d8f5e880db63493b59126a448a30a74e1 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 10 Jan 2018 10:58:35 +0100 Subject: [PATCH 172/271] Add project release info to Travis config. --- .travis.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 92538f8..83a070b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,5 +7,17 @@ python: - 3.5 - 3.6 install: pip install tox-travis codecov -script: tox -- --cov # positional args ({posargs}) to pass into tox.ini +# positional args ({posargs}) to pass into tox.ini +script: tox -- --cov after_success: codecov +deploy: + provider: pypi + user: jazzband + server: https://jazzband.co/projects/django-model-utils/upload + distributions: sdist bdist_wheel + password: + secure: JxUmEdYS8qT+7xhVyzmVD4Gkwqdz5XKxoUhKP795CWIXoJjtlGszyo6w0XfnFs0epXtd1NuCRXdhea+EqWKFDlQ3Yg7m6Y/yTQV6nMHxCPSvicROho7pAiJmfc/x+rSsPt5ag8av6+S07tOqvMnWBBefYbpHRoel78RXkm9l7Mc= + on: + tags: true + repo: jazzband/django-model-utils + python: 3.6 From 7e23e3054581a6a3ba5182871f5a0ed379e0391c Mon Sep 17 00:00:00 2001 From: Germano Massullo Date: Fri, 19 Jan 2018 10:45:33 +0100 Subject: [PATCH 173/271] Removed missing include files --- MANIFEST.in | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 351a82c..51df88e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,6 +3,4 @@ include CHANGES.rst include LICENSE.txt include MANIFEST.in include README.rst -include TODO.rst -recursive-include locale django.po -include runtests.py +include model_utils/locale/de/LC_MESSAGES/django.po From 342254748314f19d181f925e103a5a0e947c0f09 Mon Sep 17 00:00:00 2001 From: Germano Massullo Date: Fri, 19 Jan 2018 10:47:56 +0100 Subject: [PATCH 174/271] Update AUTHORS.rst --- AUTHORS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 4dd3044..a5f14ec 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -14,6 +14,7 @@ Douglas Meehan Facundo Gaich Felipe Prenholato Filipe Ximenes +Germano Massullo Gregor Müllegger ivirabyan James Oakley From d34043fd2510b2c59a6a62a9a71b5cc54266d509 Mon Sep 17 00:00:00 2001 From: Jack Cushman Date: Fri, 9 Feb 2018 14:39:14 -0500 Subject: [PATCH 175/271] Avoid fetching deferred fields in has_changed --- AUTHORS.rst | 1 + CHANGES.rst | 3 ++ docs/utilities.rst | 6 ++++ model_utils/tracker.py | 19 +++++++++++ tests/test_fields/test_field_tracker.py | 44 ++++++++++++++++++++++--- 5 files changed, 68 insertions(+), 5 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 4dd3044..a5841d5 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -42,3 +42,4 @@ Trey Hunner Karl Wan Nan Wo zyegfryed Radosław Jan Ganczarek +Jack Cushman diff --git a/CHANGES.rst b/CHANGES.rst index 922821d..a5a29e0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,9 @@ CHANGES master (unreleased) ------------------- +- Fix `FieldTracker.has_changed()` and `FieldTracker.previous()` to return + correct responses for deferred fields. + 3.1.1 (2017.12.17) ------------------ diff --git a/docs/utilities.rst b/docs/utilities.rst index 44824f5..b763ba0 100644 --- a/docs/utilities.rst +++ b/docs/utilities.rst @@ -150,6 +150,10 @@ Returns the value of the given field during the last save: Returns ``None`` when the model instance isn't saved yet. +If a field is `deferred`_, calling ``previous()`` will load the previous value from the database. + +.. _deferred: https://docs.djangoproject.com/en/2.0/ref/models/querysets/#defer + has_changed ~~~~~~~~~~~ @@ -167,6 +171,8 @@ Returns ``True`` if the given field has changed since the last save. The ``has_c The ``has_changed`` method relies on ``previous`` to determine whether a field's values has changed. +If a field is `deferred`_ and has been assigned locally, calling ``has_changed()`` +will load the previous value from the database to perform the comparison. changed ~~~~~~~ diff --git a/model_utils/tracker.py b/model_utils/tracker.py index 93e4a5d..3583a68 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -67,12 +67,31 @@ class FieldInstanceTracker(object): def has_changed(self, field): """Returns ``True`` if field has changed from currently saved value""" if field in self.fields: + # deferred fields haven't changed + if field in self.instance._deferred_fields and field not in self.instance.__dict__: + return False return self.previous(field) != self.get_field_value(field) else: raise FieldError('field "%s" not tracked' % field) def previous(self, field): """Returns currently saved value of given field""" + + # handle deferred fields that have not yet been loaded from the database + if self.instance.pk and field in self.instance._deferred_fields and field not in self.saved_data: + + # if the field has not been assigned locally, simply fetch and un-defer the value + if field not in self.instance.__dict__: + self.get_field_value(field) + + # if the field has been assigned locally, store the local value, fetch the database value, + # store database value to saved_data, and restore the local value + else: + current_value = self.get_field_value(field) + self.instance.refresh_from_db(fields=[field]) + self.saved_data[field] = deepcopy(self.get_field_value(field)) + setattr(self.instance, self.field_map[field], current_value) + return self.saved_data.get(field) def changed(self): diff --git a/tests/test_fields/test_field_tracker.py b/tests/test_fields/test_field_tracker.py index 00c28b3..5580773 100644 --- a/tests/test_fields/test_field_tracker.py +++ b/tests/test_fields/test_field_tracker.py @@ -180,20 +180,54 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): self.instance.name = 'new age' self.instance.number = 1 self.instance.save() - item = list(self.tracked_class.objects.only('name').all())[0] + item = self.tracked_class.objects.only('name').first() self.assertTrue(item._deferred_fields) - self.assertEqual(item.tracker.previous('number'), None) - self.assertTrue('number' in item._deferred_fields) + # has_changed() returns False for deferred fields, without un-deferring them. + # Use an if because ModelTracked doesn't support has_changed() in this case. + if self.tracked_class == Tracked: + self.assertFalse(item.tracker.has_changed('number')) + self.assertTrue('number' in item._deferred_fields) + # previous() un-defers field and returns value + self.assertEqual(item.tracker.previous('number'), 1) + self.assertTrue('number' not in item._deferred_fields) + + # examining a deferred field un-defers it + item = self.tracked_class.objects.only('name').first() self.assertEqual(item.number, 1) self.assertTrue('number' not in item._deferred_fields) - self.assertEqual(item.tracker.previous('number'), 1) - self.assertFalse(item.tracker.has_changed('number')) + # has_changed() returns correct values after deferred field is examined + self.assertFalse(item.tracker.has_changed('number')) item.number = 2 self.assertTrue(item.tracker.has_changed('number')) + # previous() returns correct value after deferred field is examined + self.assertEqual(item.tracker.previous('number'), 1) + + # assigning to a deferred field un-defers it + # Use an if because ModelTracked doesn't handle this case. + if self.tracked_class == Tracked: + + item = self.tracked_class.objects.only('name').first() + item.number = 2 + + # _deferred_fields is not updated by assignment + self.assertTrue('number' in item._deferred_fields) + + # previous() fetches correct value from database after deferred field is assigned + self.assertEqual(item.tracker.previous('number'), 1) + + # database fetch of previous() value doesn't affect current value + self.assertEqual(item.number, 2) + + # has_changed() returns correct values after deferred field is assigned + self.assertTrue(item.tracker.has_changed('number')) + item.number = 1 + self.assertFalse(item.tracker.has_changed('number')) + + class FieldTrackerMultipleInstancesTests(TestCase): From b15f44c2602119802c6f1b41127148e21a8272b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=A1clav=20Dohnal?= Date: Fri, 4 May 2018 13:45:53 +0200 Subject: [PATCH 176/271] add czech (cs) translations --- model_utils/locale/cs/LC_MESSAGES/django.mo | Bin 0 -> 647 bytes model_utils/locale/cs/LC_MESSAGES/django.po | 43 ++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 model_utils/locale/cs/LC_MESSAGES/django.mo create mode 100644 model_utils/locale/cs/LC_MESSAGES/django.po diff --git a/model_utils/locale/cs/LC_MESSAGES/django.mo b/model_utils/locale/cs/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..bb59a77bba2cb289b3db2e44b274ed4b1ed6b66a GIT binary patch literal 647 zcmZvZ&u-K(5XKFZe*tme%)zIs2zha`i&$j|NR$>;>WZZbhyxclnN7p)+L7%)6$%J&i)?(IV_Lub%6XbZgxT|#l~q$a|DR_Gn( z2zNq1R#;M=N=CY53sct98j17D`tbCv`y;$KP`bj|#F|%Bc(GH8=VV=NbjqVWYK-+_ z?5esH58KL#++ch1Bo~{Z0bJq{`lLLO@leP+LJoM`PY3drOr(TS>`_;TYXA53SH4}$ z(5eb`3Kxxc##^T|#61~p8tqAYksmg&czW$`H*;0%QJCI3^4YU|TMSQqR%tgSE4Flj zch^Ko$GtqAZ*6i zOh(bJau_AFm@98AzQ^D3MNVpg{DpW-n(XRyVyLV=PkORJN cRb5Dz>ges)kDg}X&vb0LIQpO!4{^(W1Ml6TlmGw# literal 0 HcmV?d00001 diff --git a/model_utils/locale/cs/LC_MESSAGES/django.po b/model_utils/locale/cs/LC_MESSAGES/django.po new file mode 100644 index 0000000..4efd414 --- /dev/null +++ b/model_utils/locale/cs/LC_MESSAGES/django.po @@ -0,0 +1,43 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: django-model-utils\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-05-04 13:40+0200\n" +"PO-Revision-Date: 2018-05-04 13:43+0200\n" +"Language: cs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" +"Last-Translator: Václav Dohnal \n" +"Language-Team: \n" +"X-Generator: Poedit 2.0.7\n" + +#: .\models.py:24 +msgid "created" +msgstr "vytvořeno" + +#: .\models.py:25 +msgid "modified" +msgstr "upraveno" + +#: .\models.py:37 +msgid "start" +msgstr "začátek" + +#: .\models.py:38 +msgid "end" +msgstr "konec" + +#: .\models.py:53 +msgid "status" +msgstr "stav" + +#: .\models.py:54 +msgid "status changed" +msgstr "změna stavu" From 248db7bde4a45b0495d8a3bfacd4968755c410c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=A1clav=20Dohnal?= Date: Fri, 4 May 2018 13:46:16 +0200 Subject: [PATCH 177/271] update czech (cs) translations --- model_utils/locale/cs/LC_MESSAGES/django.mo | Bin 647 -> 705 bytes model_utils/locale/cs/LC_MESSAGES/django.po | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/model_utils/locale/cs/LC_MESSAGES/django.mo b/model_utils/locale/cs/LC_MESSAGES/django.mo index bb59a77bba2cb289b3db2e44b274ed4b1ed6b66a..758c32d5a986f8fa4f74c53148bc5d6d32849c34 100644 GIT binary patch delta 136 zcmZo?J;*vC#B>281H&r@1_mx5?q*_OUty?18E*0y=7u& zsZdH*VqSW_Zf<@`YL0GcNoG#*#D7VQW|I>ck7$G)UYMMdSf=2TpOKfCqhM2(2;%Cc VfVmFoxrv!Mddc~@c9T_^k^l}?B@+Mu delta 77 zcmX@e+Ri#5#59eOf#DSc0|OTj2Qo1*umb5AAk6`!lYlfQkj@6uEI_&xNb>;cs)?PY TlLZ))7>y?vGai{7&y)lJq>~Iv diff --git a/model_utils/locale/cs/LC_MESSAGES/django.po b/model_utils/locale/cs/LC_MESSAGES/django.po index 4efd414..2f65819 100644 --- a/model_utils/locale/cs/LC_MESSAGES/django.po +++ b/model_utils/locale/cs/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: django-model-utils\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-05-04 13:40+0200\n" -"PO-Revision-Date: 2018-05-04 13:43+0200\n" +"PO-Revision-Date: 2018-05-04 13:46+0200\n" "Language: cs\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From d7e8144d4c7fe6d4b99b577fd9b5d0454c252652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=A1clav=20Dohnal?= Date: Fri, 4 May 2018 13:50:58 +0200 Subject: [PATCH 178/271] add missing file header --- model_utils/locale/cs/LC_MESSAGES/django.po | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/model_utils/locale/cs/LC_MESSAGES/django.po b/model_utils/locale/cs/LC_MESSAGES/django.po index 2f65819..d05da38 100644 --- a/model_utils/locale/cs/LC_MESSAGES/django.po +++ b/model_utils/locale/cs/LC_MESSAGES/django.po @@ -1,7 +1,10 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. +# Czech translations of django-model-utils +# +# This file is distributed under the same license as the django-model-utils package. +# +# Translators: +# ------------ +# Václav Dohnal , 2018. # msgid "" msgstr "" From db35475ece517918b417cbe7bb568febee4ad678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=A1clav=20Dohnal?= Date: Fri, 4 May 2018 13:51:15 +0200 Subject: [PATCH 179/271] add link to the issue tracker on Github --- model_utils/locale/cs/LC_MESSAGES/django.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model_utils/locale/cs/LC_MESSAGES/django.po b/model_utils/locale/cs/LC_MESSAGES/django.po index d05da38..67b98d7 100644 --- a/model_utils/locale/cs/LC_MESSAGES/django.po +++ b/model_utils/locale/cs/LC_MESSAGES/django.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: django-model-utils\n" -"Report-Msgid-Bugs-To: \n" +"Report-Msgid-Bugs-To: https://github.com/jazzband/django-model-utils/issues\n" "POT-Creation-Date: 2018-05-04 13:40+0200\n" "PO-Revision-Date: 2018-05-04 13:46+0200\n" "Language: cs\n" From a6ce9fcc3b590a38d70af52395e1098325ab27ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=A1clav=20Dohnal?= Date: Fri, 4 May 2018 13:51:46 +0200 Subject: [PATCH 180/271] add N/A as Language-Team --- model_utils/locale/cs/LC_MESSAGES/django.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model_utils/locale/cs/LC_MESSAGES/django.po b/model_utils/locale/cs/LC_MESSAGES/django.po index 67b98d7..eae5e9a 100644 --- a/model_utils/locale/cs/LC_MESSAGES/django.po +++ b/model_utils/locale/cs/LC_MESSAGES/django.po @@ -18,7 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" "Last-Translator: Václav Dohnal \n" -"Language-Team: \n" +"Language-Team: N/A\n" "X-Generator: Poedit 2.0.7\n" #: .\models.py:24 From 6d2ba6338725d55b3c39f6b8124f5a6a2f831772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Amaro?= Date: Wed, 9 May 2018 14:45:37 -0300 Subject: [PATCH 181/271] update changelog and versions for 3.1.2 --- CHANGES.rst | 4 ++++ model_utils/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 495d972..84ea8d7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,9 +3,13 @@ CHANGES master (unreleased) ------------------- + +3.1.2 (2018.05.09) +------------------ * Update InheritanceIterable to inherit from ModelIterable instead of BaseIterable, fixes GH-277. + 3.1.1 (2017.12.17) ------------------ diff --git a/model_utils/__init__.py b/model_utils/__init__.py index f23bd9c..f297da6 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices from .tracker import FieldTracker, ModelTracker -__version__ = '3.1.1' +__version__ = '3.1.2' From 0fc0b44c95de3e39b71a1e01a3ef36a6bc887d12 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Mon, 2 Apr 2018 17:00:03 -0700 Subject: [PATCH 182/271] Remove version checks for django<1.8. Support for older versions of django was dropped in 3.0.0. --- model_utils/managers.py | 30 +++----------- model_utils/tracker.py | 40 ++++++------------- .../test_managers/test_inheritance_manager.py | 3 -- 3 files changed, 18 insertions(+), 55 deletions(-) diff --git a/model_utils/managers.py b/model_utils/managers.py index 2f2d1d2..98a2a54 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -48,11 +48,10 @@ class InheritanceIterable(ModelIterable): class InheritanceQuerySetMixin(object): def __init__(self, *args, **kwargs): super(InheritanceQuerySetMixin, self).__init__(*args, **kwargs) - if django.VERSION > (1, 8): - self._iterable_class = InheritanceIterable + self._iterable_class = InheritanceIterable def select_subclasses(self, *subclasses): - levels = self._get_maximum_depth() + levels = None calculated_subclasses = self._get_subclasses_recurse( self.model, levels=levels) # if none were passed in, we can just short circuit and select all @@ -151,12 +150,9 @@ class InheritanceQuerySetMixin(object): recursively, returning a `list` of strings representing the relations for select_related """ - if django.VERSION < (1, 8): - related_objects = model._meta.get_all_related_objects() - else: - related_objects = [ - f for f in model._meta.get_fields() - if isinstance(f, OneToOneRel)] + related_objects = [ + f for f in model._meta.get_fields() + if isinstance(f, OneToOneRel)] rels = [ rel for rel in related_objects @@ -199,10 +195,7 @@ class InheritanceQuerySetMixin(object): related = parent_link.remote_field ancestry.insert(0, related.get_accessor_name()) if levels or levels is None: - if django.VERSION < (1, 8): - parent_model = related.parent_model - else: - parent_model = related.model + parent_model = related.model parent_link = parent_model._meta.get_ancestor_link( self.model) else: @@ -230,17 +223,6 @@ class InheritanceQuerySetMixin(object): def get_subclass(self, *args, **kwargs): return self.select_subclasses().get(*args, **kwargs) - def _get_maximum_depth(self): - """ - Under Django versions < 1.6, to avoid triggering - https://code.djangoproject.com/ticket/16572 we can only look - as far as children. - """ - levels = None - if django.VERSION < (1, 6, 0): - levels = 1 - return levels - class InheritanceQuerySet(InheritanceQuerySetMixin, QuerySet): pass diff --git a/model_utils/tracker.py b/model_utils/tracker.py index 93e4a5d..0fec85d 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -97,35 +97,19 @@ class FieldInstanceTracker(object): def _get_field_name(self): return self.field.name - if django.VERSION >= (1, 8): - self.instance._deferred_fields = self.instance.get_deferred_fields() - for field in self.instance._deferred_fields: - if django.VERSION >= (1, 10): - field_obj = getattr(self.instance.__class__, field) - else: - field_obj = self.instance.__class__.__dict__.get(field) - if isinstance(field_obj, FileDescriptor): - field_tracker = FileDescriptorTracker(field_obj.field) - setattr(self.instance.__class__, field, field_tracker) - else: - field_tracker = DeferredAttributeTracker( - field_obj.field_name, None) - setattr(self.instance.__class__, field, field_tracker) - else: - for field in self.fields: + self.instance._deferred_fields = self.instance.get_deferred_fields() + for field in self.instance._deferred_fields: + if django.VERSION >= (1, 10): + field_obj = getattr(self.instance.__class__, field) + else: field_obj = self.instance.__class__.__dict__.get(field) - if isinstance(field_obj, DeferredAttribute): - self.instance._deferred_fields.add(field) - - # Django 1.4 - if django.VERSION >= (1, 5): - model = None - else: - model = field_obj.model_ref() - - field_tracker = DeferredAttributeTracker( - field_obj.field_name, model) - setattr(self.instance.__class__, field, field_tracker) + if isinstance(field_obj, FileDescriptor): + field_tracker = FileDescriptorTracker(field_obj.field) + setattr(self.instance.__class__, field, field_tracker) + else: + field_tracker = DeferredAttributeTracker( + field_obj.field_name, None) + setattr(self.instance.__class__, field, field_tracker) class FieldTracker(object): diff --git a/tests/test_managers/test_inheritance_manager.py b/tests/test_managers/test_inheritance_manager.py index 4509175..39c694e 100644 --- a/tests/test_managers/test_inheritance_manager.py +++ b/tests/test_managers/test_inheritance_manager.py @@ -115,9 +115,6 @@ class InheritanceManagerTests(TestCase): "inheritancemanagertestchild2").get(pk=self.child1.pk) obj.inheritancemanagertestchild1 - def test_version_determining_any_depth(self): - self.assertIsNone(self.get_manager().all()._get_maximum_depth()) - def test_manually_specifying_parent_fk_including_grandchildren(self): """ given a Model which inherits from another Model, but also declares From be52bc929009b58535de75dfdb40e6aa58bb4836 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Tue, 3 Apr 2018 13:18:03 -0700 Subject: [PATCH 183/271] Add failing test for deferred attributes. --- model_utils/tracker.py | 3 +- tests/models.py | 35 +++++++++++++++ tests/test_models/test_deferred_fields.py | 53 +++++++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 tests/test_models/test_deferred_fields.py diff --git a/model_utils/tracker.py b/model_utils/tracker.py index 0fec85d..5da9dd4 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -107,8 +107,7 @@ class FieldInstanceTracker(object): field_tracker = FileDescriptorTracker(field_obj.field) setattr(self.instance.__class__, field, field_tracker) else: - field_tracker = DeferredAttributeTracker( - field_obj.field_name, None) + field_tracker = DeferredAttributeTracker(field, type(self.instance)) setattr(self.instance.__class__, field, field_tracker) diff --git a/tests/models.py b/tests/models.py index a65d499..841f612 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals, absolute_import from django.db import models +from django.db.models.query_utils import DeferredAttribute from django.db.models import Manager from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ @@ -331,3 +332,37 @@ class CustomSoftDelete(SoftDeletableModel): is_read = models.BooleanField(default=False) objects = CustomSoftDeleteManager() + + +class StringyDescriptor(object): + """ + Descriptor that returns a string version of the underlying integer value. + """ + def __init__(self, name): + self.name = name + + def __get__(self, obj, cls=None): + if obj is None: + return self + if self.name in obj.get_deferred_fields(): + # This queries the database, and sets the value on the instance. + DeferredAttribute(field_name=self.name, model=cls).__get__(obj, cls) + return str(obj.__dict__[self.name]) + + def __set__(self, obj, value): + obj.__dict__[self.name] = int(value) + + +class CustomDescriptorField(models.IntegerField): + def contribute_to_class(self, cls, name, **kwargs): + super(CustomDescriptorField, self).contribute_to_class(cls, name, **kwargs) + setattr(cls, name, StringyDescriptor(name)) + + +class ModelWithCustomDescriptor(models.Model): + custom_field = CustomDescriptorField() + tracked_custom_field = CustomDescriptorField() + regular_field = models.IntegerField() + tracked_regular_field = models.IntegerField() + + tracker = FieldTracker(fields=['tracked_custom_field', 'tracked_regular_field']) diff --git a/tests/test_models/test_deferred_fields.py b/tests/test_models/test_deferred_fields.py new file mode 100644 index 0000000..6a159be --- /dev/null +++ b/tests/test_models/test_deferred_fields.py @@ -0,0 +1,53 @@ +from __future__ import unicode_literals + +from django.test import TestCase + +from tests.models import ModelWithCustomDescriptor + + +class CustomDescriptorTests(TestCase): + def setUp(self): + self.instance = ModelWithCustomDescriptor.objects.create( + custom_field='1', + tracked_custom_field='1', + regular_field=1, + tracked_regular_field=1, + ) + + def test_custom_descriptor_works(self): + instance = self.instance + self.assertEqual(instance.custom_field, '1') + self.assertEqual(instance.__dict__['custom_field'], 1) + self.assertEqual(instance.regular_field, 1) + instance.custom_field = 2 + self.assertEqual(instance.custom_field, '2') + self.assertEqual(instance.__dict__['custom_field'], 2) + instance.save() + intance = ModelWithCustomDescriptor.objects.get(pk=instance.pk) + self.assertEqual(instance.custom_field, '2') + self.assertEqual(instance.__dict__['custom_field'], 2) + + def test_deferred(self): + instance = ModelWithCustomDescriptor.objects.only('id').get( + pk=self.instance.pk) + self.assertIn('custom_field', instance.get_deferred_fields()) + self.assertEqual(instance.custom_field, '1') + self.assertNotIn('custom_field', instance.get_deferred_fields()) + self.assertEqual(instance.regular_field, 1) + self.assertEqual(instance.tracked_custom_field, '1') + self.assertEqual(instance.tracked_regular_field, 1) + + self.assertFalse(instance.tracker.has_changed('tracked_custom_field')) + self.assertFalse(instance.tracker.has_changed('tracked_regular_field')) + + instance.tracked_custom_field = 2 + instance.tracked_regular_field = 2 + self.assertTrue(instance.tracker.has_changed('tracked_custom_field')) + self.assertTrue(instance.tracker.has_changed('tracked_regular_field')) + instance.save() + + instance = ModelWithCustomDescriptor.objects.get(pk=instance.pk) + self.assertEqual(instance.custom_field, '1') + self.assertEqual(instance.regular_field, 1) + self.assertEqual(instance.tracked_custom_field, '2') + self.assertEqual(instance.tracked_regular_field, 2) From 80b099f12918d28c297005fbbb15ed1ebaf10a12 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Tue, 3 Apr 2018 15:43:29 -0700 Subject: [PATCH 184/271] Do not override custom descriptors when present. This commit adds a collection of wrapper classes for tracking fields while still using custom descriptors that may be present. This fixes a bug where deferring a model field with a custom descriptor meant that the descriptor was overridden in all subsequent queries. --- model_utils/tracker.py | 77 ++++++++++++++++++++++- tests/test_fields/test_field_tracker.py | 15 ++++- tests/test_models/test_deferred_fields.py | 11 +++- 3 files changed, 95 insertions(+), 8 deletions(-) diff --git a/model_utils/tracker.py b/model_utils/tracker.py index 5da9dd4..095d87e 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -29,12 +29,73 @@ class DescriptorMixin(object): return self.field_name +class DescriptorWrapper(object): + + def __init__(self, field_name, descriptor, tracker_attname): + self.field_name = field_name + self.descriptor = descriptor + self.tracker_attname = tracker_attname + + def __get__(self, instance, owner): + if instance is None: + return self + was_deferred = self.field_name in instance.get_deferred_fields() + if self.descriptor: + value = self.descriptor.__get__(instance, owner) + else: + value = instance.__dict__[self.field_name] + if was_deferred: + tracker_instance = getattr(instance, self.tracker_attname) + tracker_instance.saved_data[self.field_name] = deepcopy(value) + return value + + @staticmethod + def cls_for_descriptor(descriptor): + has_set = hasattr(descriptor, '__set__') + has_del = hasattr(descriptor, '__delete__') + if has_set and has_del: + return FullDescriptorWrapper + elif has_set: + return SettableDescriptorWrapper + elif has_del: + return DeleteableDescriptorWrapper + else: + return DescriptorWrapper + + +class SettableDescriptorWrapper(DescriptorWrapper): + """ + Descriptor wrapper for descriptors with a __delete__ method. + + This should not be used for descriptors + """ + def __set__(self, instance, value): + return self.descriptor.__set__(instance, value) + + +class DeleteableDescriptorWrapper(DescriptorWrapper): + """ + Descriptor wrapper for descriptors with a __delete__ method. + + This should not be used for descriptors + """ + def __delete__(self, instance): + self.descriptor.__delete__(instance) + + +class FullDescriptorWrapper(SettableDescriptorWrapper, DeleteableDescriptorWrapper): + """ + Wrapper for descriptors with all three descriptor methods. + """ + + class FieldInstanceTracker(object): def __init__(self, instance, fields, field_map): self.instance = instance self.fields = fields self.field_map = field_map - self.init_deferred_fields() + if django.VERSION < (1, 10): + self.init_deferred_fields() def get_field_value(self, field): return getattr(self.instance, self.field_map[field]) @@ -54,10 +115,11 @@ class FieldInstanceTracker(object): def current(self, fields=None): """Returns dict of current values for all tracked fields""" if fields is None: - if self.instance._deferred_fields: + deferred_fields = self.instance._deferred_fields if django.VERSION < (1, 10) else self.instance.get_deferred_fields() + if deferred_fields: fields = [ field for field in self.fields - if field not in self.instance._deferred_fields + if field not in deferred_fields ] else: fields = self.fields @@ -135,6 +197,15 @@ class FieldTracker(object): if self.fields is None: self.fields = (field.attname for field in sender._meta.fields) self.fields = set(self.fields) + if django.VERSION >= (1, 10): + for field_name in self.fields: + if django.VERSION >= (1, 10): + descriptor = getattr(sender, field_name) + else: + descriptor = sender.__dict__.get(field_name) + wrapper_cls = DescriptorWrapper.cls_for_descriptor(descriptor) + wrapped_descriptor = wrapper_cls(field_name, descriptor, self.attname) + setattr(sender, field_name, wrapped_descriptor) self.field_map = self.get_field_map(sender) models.signals.post_init.connect(self.initialize_tracker) self.model_class = sender diff --git a/tests/test_fields/test_field_tracker.py b/tests/test_fields/test_field_tracker.py index 00c28b3..b389861 100644 --- a/tests/test_fields/test_field_tracker.py +++ b/tests/test_fields/test_field_tracker.py @@ -181,13 +181,22 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): self.instance.number = 1 self.instance.save() item = list(self.tracked_class.objects.only('name').all())[0] - self.assertTrue(item._deferred_fields) + if django.VERSION >= (1, 10): + self.assertTrue(item.get_deferred_fields()) + else: + self.assertTrue(item._deferred_fields) self.assertEqual(item.tracker.previous('number'), None) - self.assertTrue('number' in item._deferred_fields) + if django.VERSION >= (1, 10): + self.assertTrue('number' in item.get_deferred_fields()) + else: + self.assertTrue('number' in item._deferred_fields) self.assertEqual(item.number, 1) - self.assertTrue('number' not in item._deferred_fields) + if django.VERSION >= (1, 10): + self.assertTrue('number' not in item.get_deferred_fields()) + else: + self.assertTrue('number' not in item._deferred_fields) self.assertEqual(item.tracker.previous('number'), 1) self.assertFalse(item.tracker.has_changed('number')) diff --git a/tests/test_models/test_deferred_fields.py b/tests/test_models/test_deferred_fields.py index 6a159be..b235843 100644 --- a/tests/test_models/test_deferred_fields.py +++ b/tests/test_models/test_deferred_fields.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import django from django.test import TestCase from tests.models import ModelWithCustomDescriptor @@ -30,9 +31,15 @@ class CustomDescriptorTests(TestCase): def test_deferred(self): instance = ModelWithCustomDescriptor.objects.only('id').get( pk=self.instance.pk) - self.assertIn('custom_field', instance.get_deferred_fields()) + if django.VERSION >= (1, 10): + self.assertIn('custom_field', instance.get_deferred_fields()) + else: + self.assertIn('custom_field', instance._deferred_fields) self.assertEqual(instance.custom_field, '1') - self.assertNotIn('custom_field', instance.get_deferred_fields()) + if django.VERSION >= (1, 10): + self.assertNotIn('custom_field', instance.get_deferred_fields()) + else: + self.assertNotIn('custom_field', instance._deferred_fields) self.assertEqual(instance.regular_field, 1) self.assertEqual(instance.tracked_custom_field, '1') self.assertEqual(instance.tracked_regular_field, 1) From be1a7d92811f9e57fd43b14b735218612f30f6ff Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Tue, 3 Apr 2018 16:10:30 -0700 Subject: [PATCH 185/271] Update AUTHORS and CHANGES. As far as I can tell, no changes to documentation is required. --- AUTHORS.rst | 1 + CHANGES.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index fd350c0..33224c5 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -43,3 +43,4 @@ Trey Hunner Karl Wan Nan Wo zyegfryed Radosław Jan Ganczarek +Lucas Wiman diff --git a/CHANGES.rst b/CHANGES.rst index 84ea8d7..9bd6129 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,7 @@ CHANGES master (unreleased) ------------------- +- Fix handling of deferred attributes on Django 1.10+, fixes GH-278 3.1.2 (2018.05.09) ------------------ From 90ed7fc9052f46ea1007af26ff9953f3f1e198b7 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Wed, 4 Apr 2018 10:02:46 -0700 Subject: [PATCH 186/271] Improve coverage. --- model_utils/tracker.py | 33 +++++------------------ tests/models.py | 3 +++ tests/test_models/test_deferred_fields.py | 11 ++++++++ 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/model_utils/tracker.py b/model_utils/tracker.py index 095d87e..0ec3044 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -40,10 +40,7 @@ class DescriptorWrapper(object): if instance is None: return self was_deferred = self.field_name in instance.get_deferred_fields() - if self.descriptor: - value = self.descriptor.__get__(instance, owner) - else: - value = instance.__dict__[self.field_name] + value = self.descriptor.__get__(instance, owner) if was_deferred: tracker_instance = getattr(instance, self.tracker_attname) tracker_instance.saved_data[self.field_name] = deepcopy(value) @@ -53,12 +50,10 @@ class DescriptorWrapper(object): def cls_for_descriptor(descriptor): has_set = hasattr(descriptor, '__set__') has_del = hasattr(descriptor, '__delete__') - if has_set and has_del: + if has_del: return FullDescriptorWrapper elif has_set: return SettableDescriptorWrapper - elif has_del: - return DeleteableDescriptorWrapper else: return DescriptorWrapper @@ -73,20 +68,12 @@ class SettableDescriptorWrapper(DescriptorWrapper): return self.descriptor.__set__(instance, value) -class DeleteableDescriptorWrapper(DescriptorWrapper): - """ - Descriptor wrapper for descriptors with a __delete__ method. - - This should not be used for descriptors - """ - def __delete__(self, instance): - self.descriptor.__delete__(instance) - - -class FullDescriptorWrapper(SettableDescriptorWrapper, DeleteableDescriptorWrapper): +class FullDescriptorWrapper(SettableDescriptorWrapper): """ Wrapper for descriptors with all three descriptor methods. """ + def __delete__(self, obj): + self.descriptor.__delete__(obj) class FieldInstanceTracker(object): @@ -161,10 +148,7 @@ class FieldInstanceTracker(object): self.instance._deferred_fields = self.instance.get_deferred_fields() for field in self.instance._deferred_fields: - if django.VERSION >= (1, 10): - field_obj = getattr(self.instance.__class__, field) - else: - field_obj = self.instance.__class__.__dict__.get(field) + field_obj = self.instance.__class__.__dict__.get(field) if isinstance(field_obj, FileDescriptor): field_tracker = FileDescriptorTracker(field_obj.field) setattr(self.instance.__class__, field, field_tracker) @@ -199,10 +183,7 @@ class FieldTracker(object): self.fields = set(self.fields) if django.VERSION >= (1, 10): for field_name in self.fields: - if django.VERSION >= (1, 10): - descriptor = getattr(sender, field_name) - else: - descriptor = sender.__dict__.get(field_name) + descriptor = getattr(sender, field_name) wrapper_cls = DescriptorWrapper.cls_for_descriptor(descriptor) wrapped_descriptor = wrapper_cls(field_name, descriptor, self.attname) setattr(sender, field_name, wrapped_descriptor) diff --git a/tests/models.py b/tests/models.py index 841f612..91df0b6 100644 --- a/tests/models.py +++ b/tests/models.py @@ -352,6 +352,9 @@ class StringyDescriptor(object): def __set__(self, obj, value): obj.__dict__[self.name] = int(value) + def __delete__(self, obj): + del obj.__dict__[self.name] + class CustomDescriptorField(models.IntegerField): def contribute_to_class(self, cls, name, **kwargs): diff --git a/tests/test_models/test_deferred_fields.py b/tests/test_models/test_deferred_fields.py index b235843..05ba336 100644 --- a/tests/test_models/test_deferred_fields.py +++ b/tests/test_models/test_deferred_fields.py @@ -58,3 +58,14 @@ class CustomDescriptorTests(TestCase): self.assertEqual(instance.regular_field, 1) self.assertEqual(instance.tracked_custom_field, '2') self.assertEqual(instance.tracked_regular_field, 2) + + instance = ModelWithCustomDescriptor.objects.only('id').get(pk=instance.pk) + if django.VERSION >= (1, 10): + # This fails on 1.8 and 1.9, which is a bug in the deferred field + # implementation on those versions. + instance.tracked_custom_field = 3 + self.assertEqual(instance.tracked_custom_field, '3') + self.assertTrue(instance.tracker.has_changed('tracked_custom_field')) + del instance.tracked_custom_field + self.assertEqual(instance.tracked_custom_field, '2') + self.assertFalse(instance.tracker.has_changed('tracked_custom_field')) From 5d410e9ccceb092947ae9c0777e4b42b93b8376a Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Thu, 21 Jun 2018 13:07:13 -0700 Subject: [PATCH 187/271] Fix test failures from merge. --- model_utils/tracker.py | 10 +++++++--- tests/test_fields/test_field_tracker.py | 9 ++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/model_utils/tracker.py b/model_utils/tracker.py index d1101e2..e69d177 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -84,6 +84,10 @@ class FieldInstanceTracker(object): if django.VERSION < (1, 10): self.init_deferred_fields() + @property + def deferred_fields(self): + return self.instance._deferred_fields if django.VERSION < (1, 10) else self.instance.get_deferred_fields() + def get_field_value(self, field): return getattr(self.instance, self.field_map[field]) @@ -102,7 +106,7 @@ class FieldInstanceTracker(object): def current(self, fields=None): """Returns dict of current values for all tracked fields""" if fields is None: - deferred_fields = self.instance._deferred_fields if django.VERSION < (1, 10) else self.instance.get_deferred_fields() + deferred_fields = self.deferred_fields if deferred_fields: fields = [ field for field in self.fields @@ -117,7 +121,7 @@ class FieldInstanceTracker(object): """Returns ``True`` if field has changed from currently saved value""" if field in self.fields: # deferred fields haven't changed - if field in self.instance._deferred_fields and field not in self.instance.__dict__: + if field in self.deferred_fields and field not in self.instance.__dict__: return False return self.previous(field) != self.get_field_value(field) else: @@ -127,7 +131,7 @@ class FieldInstanceTracker(object): """Returns currently saved value of given field""" # handle deferred fields that have not yet been loaded from the database - if self.instance.pk and field in self.instance._deferred_fields and field not in self.saved_data: + if self.instance.pk and field in self.deferred_fields and field not in self.saved_data: # if the field has not been assigned locally, simply fetch and un-defer the value if field not in self.instance.__dict__: diff --git a/tests/test_fields/test_field_tracker.py b/tests/test_fields/test_field_tracker.py index bc0da4c..dc29e70 100644 --- a/tests/test_fields/test_field_tracker.py +++ b/tests/test_fields/test_field_tracker.py @@ -189,7 +189,6 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): # has_changed() returns False for deferred fields, without un-deferring them. # Use an if because ModelTracked doesn't support has_changed() in this case. if self.tracked_class == Tracked: - self.assertEqual(item.tracker.previous('number'), None) if django.VERSION >= (1, 10): self.assertTrue('number' in item.get_deferred_fields()) else: @@ -197,7 +196,10 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): # previous() un-defers field and returns value self.assertEqual(item.tracker.previous('number'), 1) - self.assertTrue('number' not in item._deferred_fields) + if django.VERSION >= (1, 10): + self.assertNotIn('number', item.get_deferred_fields()) + else: + self.assertNotIn('number', item._deferred_fields) # examining a deferred field un-defers it item = self.tracked_class.objects.only('name').first() @@ -224,9 +226,6 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): item = self.tracked_class.objects.only('name').first() item.number = 2 - # _deferred_fields is not updated by assignment - self.assertTrue('number' in item._deferred_fields) - # previous() fetches correct value from database after deferred field is assigned self.assertEqual(item.tracker.previous('number'), 1) From a84c3afdddfe75520ac2caf92d99bda580c793bc Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Thu, 28 Jun 2018 13:09:42 -0700 Subject: [PATCH 188/271] Fix behavior of .previous() in Django 1.10+. The complications are that when the attribute is set in Django 1.10, it no longer counts as a deferred attribute, and it is not retrieved from the database. Naively updating __set__ to retrieve the value if it is deferred leads to infinite recursion because accessing the attribute involves loading data from the database and trying to set the attribute based on that value. This commit introduces a somewhat hacky flag that records whether we're already trying to set the attribute further up in the call stack. --- model_utils/tracker.py | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/model_utils/tracker.py b/model_utils/tracker.py index e69d177..a145b1a 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -46,29 +46,40 @@ class DescriptorWrapper(object): tracker_instance.saved_data[self.field_name] = deepcopy(value) return value + def __set__(self, instance, value): + initialized = hasattr(instance, '_instance_intialized') + was_deferred = self.field_name in instance.get_deferred_fields() + + # Sentinel attribute to detect whether we are already trying to + # set the attribute higher up the stack. This prevents infinite + # recursion when retrieving deferred values from the database. + recursion_sentinel_attname = '_setting_' + self.field_name + already_setting = hasattr(instance, recursion_sentinel_attname) + + if initialized and was_deferred and not already_setting: + setattr(instance, recursion_sentinel_attname, True) + try: + # Retrieve the value to set the saved_data value. + # This will undefer the field + getattr(instance, self.field_name) + finally: + if already_setting: + instance.__dict__.pop(recursion_sentinel_attname, None) + if hasattr(self.descriptor, '__set__'): + self.descriptor.__set__(instance, value) + else: + instance.__dict__[self.field_name] = value + @staticmethod def cls_for_descriptor(descriptor): - has_set = hasattr(descriptor, '__set__') has_del = hasattr(descriptor, '__delete__') if has_del: return FullDescriptorWrapper - elif has_set: - return SettableDescriptorWrapper else: return DescriptorWrapper -class SettableDescriptorWrapper(DescriptorWrapper): - """ - Descriptor wrapper for descriptors with a __delete__ method. - - This should not be used for descriptors - """ - def __set__(self, instance, value): - return self.descriptor.__set__(instance, value) - - -class FullDescriptorWrapper(SettableDescriptorWrapper): +class FullDescriptorWrapper(DescriptorWrapper): """ Wrapper for descriptors with all three descriptor methods. """ @@ -222,6 +233,7 @@ class FieldTracker(object): setattr(instance, self.attname, tracker) tracker.set_saved_fields() self.patch_save(instance) + instance._instance_intialized = True def patch_save(self, instance): original_save = instance.save From 15f9393bb21e2ea349113868d38eeb180dee97cb Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Thu, 28 Jun 2018 13:16:33 -0700 Subject: [PATCH 189/271] Handle API change in DeferredAttribute descriptor in django-trunk. This should maintain compatibility with the next version of django. --- model_utils/tracker.py | 5 ++++- tests/models.py | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/model_utils/tracker.py b/model_utils/tracker.py index a145b1a..a0bc0d0 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -187,7 +187,10 @@ class FieldInstanceTracker(object): field_tracker = FileDescriptorTracker(field_obj.field) setattr(self.instance.__class__, field, field_tracker) else: - field_tracker = DeferredAttributeTracker(field, type(self.instance)) + if django.VERSION < (2, 1): + field_tracker = DeferredAttributeTracker(field, type(self.instance)) + else: + field_tracker = DeferredAttributeTracker(field) setattr(self.instance.__class__, field, field_tracker) diff --git a/tests/models.py b/tests/models.py index 91df0b6..7c9bb56 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals, absolute_import +import django from django.db import models from django.db.models.query_utils import DeferredAttribute from django.db.models import Manager @@ -346,7 +347,10 @@ class StringyDescriptor(object): return self if self.name in obj.get_deferred_fields(): # This queries the database, and sets the value on the instance. - DeferredAttribute(field_name=self.name, model=cls).__get__(obj, cls) + if django.VERSION < (2, 1): + DeferredAttribute(field_name=self.name, model=cls).__get__(obj, cls) + else: + DeferredAttribute(field_name=self.name).__get__(obj, cls) return str(obj.__dict__[self.name]) def __set__(self, obj, value): From 4740ab43ec161cbe5b80da5bc6ee59c644161b16 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Thu, 28 Jun 2018 13:41:09 -0700 Subject: [PATCH 190/271] Update passed environment variables to match codecov documentation. Hopefully this will combine the coverage reports. --- tox.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tox.ini b/tox.ini index fe42bb1..861074c 100644 --- a/tox.ini +++ b/tox.ini @@ -18,6 +18,10 @@ deps = pytest-cov ignore_outcome = djangotrunk: True +passenv = + CI + TRAVIS + TRAVIS_* commands = pip install -e . From c16a275bd7c04d42ac35ede7fb7be005a479d7dd Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Thu, 28 Jun 2018 13:46:39 -0700 Subject: [PATCH 191/271] Use --cov-append option in travis build to include coverage data from all tox environments run on the travis environment. Note that it is run as one per python version, but multiple versions of django are tested in each. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 83a070b..295d123 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ python: - 3.6 install: pip install tox-travis codecov # positional args ({posargs}) to pass into tox.ini -script: tox -- --cov +script: tox -- --cov --cov-append after_success: codecov deploy: provider: pypi From 59347ef36fc032717ffa462bb56edd17b2a09d45 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Thu, 28 Jun 2018 13:52:52 -0700 Subject: [PATCH 192/271] Correctly clean up recursion sentinel value. --- model_utils/tracker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/model_utils/tracker.py b/model_utils/tracker.py index a0bc0d0..e1e61fe 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -63,8 +63,7 @@ class DescriptorWrapper(object): # This will undefer the field getattr(instance, self.field_name) finally: - if already_setting: - instance.__dict__.pop(recursion_sentinel_attname, None) + instance.__dict__.pop(recursion_sentinel_attname, None) if hasattr(self.descriptor, '__set__'): self.descriptor.__set__(instance, value) else: From cde1d706afb16ef3a48eeef414310da9ae75aa36 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Thu, 28 Jun 2018 14:08:03 -0700 Subject: [PATCH 193/271] Cover a branch in `has_changed`. --- tests/test_fields/test_field_tracker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_fields/test_field_tracker.py b/tests/test_fields/test_field_tracker.py index dc29e70..604ec60 100644 --- a/tests/test_fields/test_field_tracker.py +++ b/tests/test_fields/test_field_tracker.py @@ -189,6 +189,7 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): # has_changed() returns False for deferred fields, without un-deferring them. # Use an if because ModelTracked doesn't support has_changed() in this case. if self.tracked_class == Tracked: + self.assertFalse(item.tracker.has_changed('number')) if django.VERSION >= (1, 10): self.assertTrue('number' in item.get_deferred_fields()) else: From ca2fbb4ccd27e85dbe95744bd3d1fe5412a4d72d Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Thu, 28 Jun 2018 16:59:30 -0700 Subject: [PATCH 194/271] Fix coverage for a codepath only executed in <1.10 environments. --- model_utils/tracker.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/model_utils/tracker.py b/model_utils/tracker.py index e1e61fe..9c6de5a 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -186,10 +186,7 @@ class FieldInstanceTracker(object): field_tracker = FileDescriptorTracker(field_obj.field) setattr(self.instance.__class__, field, field_tracker) else: - if django.VERSION < (2, 1): - field_tracker = DeferredAttributeTracker(field, type(self.instance)) - else: - field_tracker = DeferredAttributeTracker(field) + field_tracker = DeferredAttributeTracker(field, type(self.instance)) setattr(self.instance.__class__, field, field_tracker) From 7d6b45f0c1943c1af50c60305ccfdd703f4290ea Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Thu, 28 Jun 2018 17:04:57 -0700 Subject: [PATCH 195/271] Increase coverage: verify that accessing the descriptor from the class yields the descriptor object. --- model_utils/tracker.py | 3 +-- tests/test_fields/test_field_tracker.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/model_utils/tracker.py b/model_utils/tracker.py index 9c6de5a..582ae06 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -71,8 +71,7 @@ class DescriptorWrapper(object): @staticmethod def cls_for_descriptor(descriptor): - has_del = hasattr(descriptor, '__delete__') - if has_del: + if hasattr(descriptor, '__delete__'): return FullDescriptorWrapper else: return DescriptorWrapper diff --git a/tests/test_fields/test_field_tracker.py b/tests/test_fields/test_field_tracker.py index 604ec60..93b9efa 100644 --- a/tests/test_fields/test_field_tracker.py +++ b/tests/test_fields/test_field_tracker.py @@ -7,6 +7,7 @@ from django.core.exceptions import FieldError from django.test import TestCase from model_utils import FieldTracker +from model_utils.tracker import DescriptorWrapper from tests.models import ( Tracked, TrackedFK, InheritedTrackedFK, TrackedNotDefault, TrackedNonFieldAttr, TrackedMultiple, InheritedTracked, TrackedFileField, @@ -191,6 +192,7 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): if self.tracked_class == Tracked: self.assertFalse(item.tracker.has_changed('number')) if django.VERSION >= (1, 10): + self.assertIsInstance(item.__class__.number, DescriptorWrapper) self.assertTrue('number' in item.get_deferred_fields()) else: self.assertTrue('number' in item._deferred_fields) From 45502c0ec2044f3a35183756d3b46ce456486853 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Sat, 30 Jun 2018 15:52:00 -0700 Subject: [PATCH 196/271] Put changelog in the right place. --- CHANGES.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b367ada..85cf4cb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,16 +4,14 @@ CHANGES master (unreleased) ------------------- - Fix handling of deferred attributes on Django 1.10+, fixes GH-278 +- Fix `FieldTracker.has_changed()` and `FieldTracker.previous()` to return + correct responses for deferred fields. 3.1.2 (2018.05.09) ------------------ * Update InheritanceIterable to inherit from ModelIterable instead of BaseIterable, fixes GH-277. - -- Fix `FieldTracker.has_changed()` and `FieldTracker.previous()` to return - correct responses for deferred fields. - 3.1.1 (2017.12.17) ------------------ From 11978397653addd1457867acbab958df436fc12a Mon Sep 17 00:00:00 2001 From: Harry Moreno Date: Tue, 26 Jun 2018 18:32:15 -0400 Subject: [PATCH 197/271] Update docs to support django 2.0 --- docs/setup.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/setup.rst b/docs/setup.rst index db2a34e..1335bc5 100644 --- a/docs/setup.rst +++ b/docs/setup.rst @@ -17,7 +17,7 @@ modify your ``INSTALLED_APPS`` setting. Dependencies ============ -``django-model-utils`` supports `Django`_ 1.8 through 1.10 (latest bugfix +``django-model-utils`` supports `Django`_ 1.8 through 2.0 (latest bugfix release in each series only) on Python 2.7, 3.3 (Django 1.8 only), 3.4 and 3.5. .. _Django: http://www.djangoproject.com/ From e750fc7408b9b9dc542b41d820cbf5417389eb43 Mon Sep 17 00:00:00 2001 From: Martey Dodoo Date: Thu, 26 Apr 2018 16:42:43 -0400 Subject: [PATCH 198/271] Fix AUTHORS.rst formatting. Add pipe character at the beginning of each line so that all authors are not concatenated together when ReStructuredText is parsed. Add Martey to AUTHORS as well. --- AUTHORS.rst | 91 +++++++++++++++++++++++++++-------------------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index fd350c0..99c50b4 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,45 +1,46 @@ -ad-m -Alejandro Varas -Alex Orange -Alexey Evseev -Andy Freeland -Artis Avotins -Bram Boogaard -Carl Meyer -Curtis Maloney -Den Lesnov -Dmytro Kyrychuk -Donald Stufft -Douglas Meehan -Facundo Gaich -Felipe Prenholato -Filipe Ximenes -Gregor Müllegger -Hanley Hansen -ivirabyan -James Oakley -Jannis Leidel -Jarek Glowacki -Javier García Sogo -Jeff Elmore -Keryn Knight -Matthew Schinckel -Michael van Tellingen -Mike Bryant -Mikhail Silonov -Patryk Zawadzki -Paul McLanahan -Philipp Steinhardt -Rinat Shigapov -Rodney Folz -Romain Garrigues -rsenkbeil -Ryan Kaskel -Simon Meers -sayane -Tony Aldridge -Travis Swicegood -Trey Hunner -Karl Wan Nan Wo -zyegfryed -Radosław Jan Ganczarek +| ad-m +| Alejandro Varas +| Alex Orange +| Alexey Evseev +| Andy Freeland +| Artis Avotins +| Bram Boogaard +| Carl Meyer +| Curtis Maloney +| Den Lesnov +| Dmytro Kyrychuk +| Donald Stufft +| Douglas Meehan +| Facundo Gaich +| Felipe Prenholato +| Filipe Ximenes +| Gregor Müllegger +| Hanley Hansen +| ivirabyan +| James Oakley +| Jannis Leidel +| Jarek Glowacki +| Javier García Sogo +| Jeff Elmore +| Keryn Knight +| Martey Dodoo +| Matthew Schinckel +| Michael van Tellingen +| Mike Bryant +| Mikhail Silonov +| Patryk Zawadzki +| Paul McLanahan +| Philipp Steinhardt +| Rinat Shigapov +| Rodney Folz +| Romain Garrigues +| rsenkbeil +| Ryan Kaskel +| Simon Meers +| sayane +| Tony Aldridge +| Travis Swicegood +| Trey Hunner +| Karl Wan Nan Wo +| zyegfryed +| Radosław Jan Ganczarek From 54543f1e8d3813652a22b745b87848ca6dad7c2f Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Mon, 2 Jul 2018 11:43:38 -0700 Subject: [PATCH 199/271] Add a flake8 environment to ensure pep8 compatibility. --- tox.ini | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 861074c..aaa2ca7 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,7 @@ envlist = py34-django{18,19,110,111,200} py35-django{18,19,110,111,200,trunk} py36-django{111,200,trunk} + flake8 [testenv] deps = @@ -19,10 +20,18 @@ deps = ignore_outcome = djangotrunk: True passenv = - CI - TRAVIS - TRAVIS_* + CI + TRAVIS + TRAVIS_* commands = pip install -e . py.test {posargs} + +[testenv:flake8] +basepython = + python3.6 +deps = + flake8 +commands = + flake8 model_utils tests From 9a6634b87a3e13b6b7cddf4a0f38bbca5acd1259 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Mon, 2 Jul 2018 11:45:51 -0700 Subject: [PATCH 200/271] Ignore assigning to lambda warning. --- tox.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tox.ini b/tox.ini index aaa2ca7..6c6da4e 100644 --- a/tox.ini +++ b/tox.ini @@ -35,3 +35,7 @@ deps = flake8 commands = flake8 model_utils tests + +[flake8] +ignore = + E731 From 654e13235e3f1dd5bcafc2b76729c9b79486b439 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Mon, 2 Jul 2018 11:47:19 -0700 Subject: [PATCH 201/271] Fix E303 too many blank lines. --- model_utils/choices.py | 12 ------------ tests/test_fields/test_field_tracker.py | 1 - 2 files changed, 13 deletions(-) diff --git a/model_utils/choices.py b/model_utils/choices.py index d48ba90..c036cf4 100644 --- a/model_utils/choices.py +++ b/model_utils/choices.py @@ -57,7 +57,6 @@ class Choices(object): self._process(choices) - def _store(self, triple, triple_collector, double_collector): self._identifier_map[triple[1]] = triple[0] self._display_map[triple[0]] = triple[2] @@ -65,7 +64,6 @@ class Choices(object): triple_collector.append(triple) double_collector.append((triple[0], triple[2])) - def _process(self, choices, triple_collector=None, double_collector=None): if triple_collector is None: triple_collector = self._triples @@ -98,26 +96,21 @@ class Choices(object): else: store((choice, choice, choice)) - def __len__(self): return len(self._doubles) - def __iter__(self): return iter(self._doubles) - def __getattr__(self, attname): try: return self._identifier_map[attname] except KeyError: raise AttributeError(attname) - def __getitem__(self, key): return self._display_map[key] - def __add__(self, other): if isinstance(other, self.__class__): other = other._triples @@ -125,29 +118,24 @@ class Choices(object): other = list(other) return Choices(*(self._triples + other)) - def __radd__(self, other): # radd is never called for matching types, so we don't check here other = list(other) return Choices(*(other + self._triples)) - def __eq__(self, other): if isinstance(other, self.__class__): return self._triples == other._triples return False - def __repr__(self): return '%s(%s)' % ( self.__class__.__name__, ', '.join(("%s" % repr(i) for i in self._triples)) ) - def __contains__(self, item): return item in self._db_values - def __deepcopy__(self, memo): return self.__class__(*copy.deepcopy(self._triples, memo)) diff --git a/tests/test_fields/test_field_tracker.py b/tests/test_fields/test_field_tracker.py index 93b9efa..cff2e35 100644 --- a/tests/test_fields/test_field_tracker.py +++ b/tests/test_fields/test_field_tracker.py @@ -241,7 +241,6 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): self.assertFalse(item.tracker.has_changed('number')) - class FieldTrackerMultipleInstancesTests(TestCase): def test_with_deferred_fields_access_multiple(self): From f845dcb24c924531503f9695f63ea61cf4c84d01 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Mon, 2 Jul 2018 11:49:16 -0700 Subject: [PATCH 202/271] Fix E231: missing whitespace after ",". --- tests/test_fields/test_field_tracker.py | 52 ++++++++++++------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/tests/test_fields/test_field_tracker.py b/tests/test_fields/test_field_tracker.py index cff2e35..b0f2dba 100644 --- a/tests/test_fields/test_field_tracker.py +++ b/tests/test_fields/test_field_tracker.py @@ -75,7 +75,7 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): self.assertChanged(name=None, number=None) self.instance.name = '' self.assertChanged(name=None, number=None) - self.instance.mutable = [1,2,3] + self.instance.mutable = [1, 2, 3] self.assertChanged(name=None, number=None, mutable=None) def test_pre_save_has_changed(self): @@ -84,7 +84,7 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): self.assertHasChanged(name=True, number=False, mutable=False) self.instance.number = 7 self.assertHasChanged(name=True, number=True) - self.instance.mutable = [1,2,3] + self.instance.mutable = [1, 2, 3] self.assertHasChanged(name=True, number=True, mutable=True) def test_first_save(self): @@ -94,22 +94,22 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): self.assertChanged(name=None) self.instance.name = 'retro' self.instance.number = 4 - self.instance.mutable = [1,2,3] + self.instance.mutable = [1, 2, 3] self.assertHasChanged(name=True, number=True, mutable=True) self.assertPrevious(name=None, number=None, mutable=None) - self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) + self.assertCurrent(name='retro', number=4, id=None, mutable=[1, 2, 3]) self.assertChanged(name=None, number=None, mutable=None) self.instance.save(update_fields=[]) self.assertHasChanged(name=True, number=True, mutable=True) self.assertPrevious(name=None, number=None, mutable=None) - self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) + self.assertCurrent(name='retro', number=4, id=None, mutable=[1, 2, 3]) self.assertChanged(name=None, number=None, mutable=None) with self.assertRaises(ValueError): self.instance.save(update_fields=['number']) def test_post_save_has_changed(self): - self.update_instance(name='retro', number=4, mutable=[1,2,3]) + self.update_instance(name='retro', number=4, mutable=[1, 2, 3]) self.assertHasChanged(name=False, number=False, mutable=False) self.instance.name = 'new age' self.assertHasChanged(name=True, number=False) @@ -121,14 +121,14 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): self.assertHasChanged(name=False, number=True, mutable=True) def test_post_save_previous(self): - self.update_instance(name='retro', number=4, mutable=[1,2,3]) + self.update_instance(name='retro', number=4, mutable=[1, 2, 3]) self.instance.name = 'new age' - self.assertPrevious(name='retro', number=4, mutable=[1,2,3]) + self.assertPrevious(name='retro', number=4, mutable=[1, 2, 3]) self.instance.mutable[1] = 4 - self.assertPrevious(name='retro', number=4, mutable=[1,2,3]) + self.assertPrevious(name='retro', number=4, mutable=[1, 2, 3]) def test_post_save_changed(self): - self.update_instance(name='retro', number=4, mutable=[1,2,3]) + self.update_instance(name='retro', number=4, mutable=[1, 2, 3]) self.assertChanged() self.instance.name = 'new age' self.assertChanged(name='retro') @@ -137,8 +137,8 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): self.instance.name = 'retro' self.assertChanged(number=4) self.instance.mutable[1] = 4 - self.assertChanged(number=4, mutable=[1,2,3]) - self.instance.mutable = [1,2,3] + self.assertChanged(number=4, mutable=[1, 2, 3]) + self.instance.mutable = [1, 2, 3] self.assertChanged(number=4) def test_current(self): @@ -147,29 +147,29 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): self.assertCurrent(id=None, name='new age', number=None, mutable=None) self.instance.number = 8 self.assertCurrent(id=None, name='new age', number=8, mutable=None) - self.instance.mutable = [1,2,3] - self.assertCurrent(id=None, name='new age', number=8, mutable=[1,2,3]) + self.instance.mutable = [1, 2, 3] + self.assertCurrent(id=None, name='new age', number=8, mutable=[1, 2, 3]) self.instance.mutable[1] = 4 - self.assertCurrent(id=None, name='new age', number=8, mutable=[1,4,3]) + self.assertCurrent(id=None, name='new age', number=8, mutable=[1, 4, 3]) self.instance.save() - self.assertCurrent(id=self.instance.id, name='new age', number=8, mutable=[1,4,3]) + self.assertCurrent(id=self.instance.id, name='new age', number=8, mutable=[1, 4, 3]) def test_update_fields(self): - self.update_instance(name='retro', number=4, mutable=[1,2,3]) + self.update_instance(name='retro', number=4, mutable=[1, 2, 3]) self.assertChanged() self.instance.name = 'new age' self.instance.number = 8 - self.instance.mutable = [4,5,6] - self.assertChanged(name='retro', number=4, mutable=[1,2,3]) + self.instance.mutable = [4, 5, 6] + self.assertChanged(name='retro', number=4, mutable=[1, 2, 3]) self.instance.save(update_fields=[]) - self.assertChanged(name='retro', number=4, mutable=[1,2,3]) + self.assertChanged(name='retro', number=4, mutable=[1, 2, 3]) self.instance.save(update_fields=['name']) in_db = self.tracked_class.objects.get(id=self.instance.id) self.assertEqual(in_db.name, self.instance.name) self.assertNotEqual(in_db.number, self.instance.number) - self.assertChanged(number=4, mutable=[1,2,3]) + self.assertChanged(number=4, mutable=[1, 2, 3]) self.instance.save(update_fields=['number']) - self.assertChanged(mutable=[1,2,3]) + self.assertChanged(mutable=[1, 2, 3]) self.instance.save(update_fields=['mutable']) self.assertChanged() in_db = self.tracked_class.objects.get(id=self.instance.id) @@ -649,7 +649,7 @@ class ModelTrackerTests(FieldTrackerTests): self.assertChanged() self.instance.name = '' self.assertChanged() - self.instance.mutable = [1,2,3] + self.instance.mutable = [1, 2, 3] self.assertChanged() def test_first_save(self): @@ -659,16 +659,16 @@ class ModelTrackerTests(FieldTrackerTests): self.assertChanged() self.instance.name = 'retro' self.instance.number = 4 - self.instance.mutable = [1,2,3] + self.instance.mutable = [1, 2, 3] self.assertHasChanged(name=True, number=True, mutable=True) self.assertPrevious(name=None, number=None, mutable=None) - self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) + self.assertCurrent(name='retro', number=4, id=None, mutable=[1, 2, 3]) self.assertChanged() self.instance.save(update_fields=[]) self.assertHasChanged(name=True, number=True, mutable=True) self.assertPrevious(name=None, number=None, mutable=None) - self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) + self.assertCurrent(name='retro', number=4, id=None, mutable=[1, 2, 3]) self.assertChanged() with self.assertRaises(ValueError): self.instance.save(update_fields=['number']) From 954624cb2221ece1fc199e55d21c3aaf159c8343 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Mon, 2 Jul 2018 11:50:56 -0700 Subject: [PATCH 203/271] Fix F841: local variable is assigned but never used. --- tests/test_models/test_deferred_fields.py | 2 +- tests/test_models/test_status_model.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_models/test_deferred_fields.py b/tests/test_models/test_deferred_fields.py index 05ba336..ea8e3bd 100644 --- a/tests/test_models/test_deferred_fields.py +++ b/tests/test_models/test_deferred_fields.py @@ -24,7 +24,7 @@ class CustomDescriptorTests(TestCase): self.assertEqual(instance.custom_field, '2') self.assertEqual(instance.__dict__['custom_field'], 2) instance.save() - intance = ModelWithCustomDescriptor.objects.get(pk=instance.pk) + instance = ModelWithCustomDescriptor.objects.get(pk=instance.pk) self.assertEqual(instance.custom_field, '2') self.assertEqual(instance.__dict__['custom_field'], 2) diff --git a/tests/test_models/test_status_model.py b/tests/test_models/test_status_model.py index f660936..6950dbf 100644 --- a/tests/test_models/test_status_model.py +++ b/tests/test_models/test_status_model.py @@ -18,7 +18,7 @@ class StatusModelTests(TestCase): c1 = self.model.objects.create() self.assertTrue(c1.status_changed, datetime(2016, 1, 1)) - c2 = self.model.objects.create() + self.model.objects.create() self.assertEqual(self.model.active.count(), 2) self.assertEqual(self.model.deleted.count(), 0) From 679aed41a298d6e97cf8abfb1ee3248d65b542fd Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Mon, 2 Jul 2018 11:52:05 -0700 Subject: [PATCH 204/271] Remove unused __unicode__ method (dead code). --- tests/models.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/models.py b/tests/models.py index 7c9bb56..80f3a3f 100644 --- a/tests/models.py +++ b/tests/models.py @@ -38,9 +38,6 @@ class InheritanceManagerTestParent(models.Model): on_delete=models.CASCADE) objects = InheritanceManager() - def __unicode__(self): - return unicode(self.pk) - def __str__(self): return "%s(%s)" % ( self.__class__.__name__[len('InheritanceManagerTest'):], From 600ddc8dc546f046b461a8c7cae9af9f26dc7ac8 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Mon, 2 Jul 2018 12:16:41 -0700 Subject: [PATCH 205/271] Fix F401: imported but unused. --- model_utils/__init__.py | 4 ++-- tests/test_fields/test_field_tracker.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/model_utils/__init__.py b/model_utils/__init__.py index f297da6..54f1b02 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ -from .choices import Choices -from .tracker import FieldTracker, ModelTracker +from .choices import Choices # noqa:F401 +from .tracker import FieldTracker, ModelTracker # noqa:F401 __version__ = '3.1.2' diff --git a/tests/test_fields/test_field_tracker.py b/tests/test_fields/test_field_tracker.py index b0f2dba..43f12f5 100644 --- a/tests/test_fields/test_field_tracker.py +++ b/tests/test_fields/test_field_tracker.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -from unittest import skipUnless - import django from django.core.exceptions import FieldError from django.test import TestCase From da65d0be3245df66a4ba3f687d35d2cac92f50ee Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Mon, 2 Jul 2018 12:20:38 -0700 Subject: [PATCH 206/271] Fix E30X too-few line spacing errors. --- model_utils/fields.py | 12 +++++++++--- model_utils/tracker.py | 2 ++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/model_utils/fields.py b/model_utils/fields.py index f308706..d9d9b85 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -141,6 +141,7 @@ SPLIT_DEFAULT_PARAGRAPHS = getattr(settings, 'SPLIT_DEFAULT_PARAGRAPHS', 2) _excerpt_field_name = lambda name: '_%s_excerpt' % name + def get_excerpt(content): excerpt = [] default_excerpt = [] @@ -156,6 +157,7 @@ def get_excerpt(content): return '\n'.join(default_excerpt) + @python_2_unicode_compatible class SplitText(object): def __init__(self, instance, field_name, excerpt_field_name): @@ -166,11 +168,13 @@ class SplitText(object): self.excerpt_field_name = excerpt_field_name # content is read/write - def _get_content(self): + @property + def content(self): return self.instance.__dict__[self.field_name] - def _set_content(self, val): + + @content.setter + def content(self, val): setattr(self.instance, self.field_name, val) - content = property(_get_content, _set_content) # excerpt is a read only property def _get_excerpt(self): @@ -185,6 +189,7 @@ class SplitText(object): def __str__(self): return self.content + class SplitDescriptor(object): def __init__(self, field): self.field = field @@ -205,6 +210,7 @@ class SplitDescriptor(object): else: obj.__dict__[self.field.name] = value + class SplitField(models.TextField): def __init__(self, *args, **kwargs): # for South FakeORM compatibility: the frozen version of a diff --git a/model_utils/tracker.py b/model_utils/tracker.py index 582ae06..8ce10dc 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -235,6 +235,7 @@ class FieldTracker(object): def patch_save(self, instance): original_save = instance.save + def save(**kwargs): ret = original_save(**kwargs) update_fields = kwargs.get('update_fields') @@ -251,6 +252,7 @@ class FieldTracker(object): fields=fields ) return ret + instance.save = save def __get__(self, instance, owner): From e23e86a2beb13a66884d1670fd11a8988a605cc9 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Mon, 2 Jul 2018 12:52:47 -0700 Subject: [PATCH 207/271] Ignore W503 line break before binary operator It doesn't seem like following this rule will lead to clearer code in the violations in this codebase. --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6c6da4e..c46b78a 100644 --- a/tox.ini +++ b/tox.ini @@ -38,4 +38,5 @@ commands = [flake8] ignore = - E731 + E731, ; do not assign a lambda expression, use a def + W503 ; line break before binary operator From 0859508a6422bc57b54060ee504f3166e8536d9e Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Mon, 2 Jul 2018 12:57:14 -0700 Subject: [PATCH 208/271] Fix E123 closing bracket does not match indentation of opening bracket's line --- model_utils/choices.py | 4 ++-- model_utils/managers.py | 4 ++-- tests/test_fields/test_status_field.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/model_utils/choices.py b/model_utils/choices.py index c036cf4..681e44b 100644 --- a/model_utils/choices.py +++ b/model_utils/choices.py @@ -92,7 +92,7 @@ class Choices(object): raise ValueError( "Choices can't take a list of length %s, only 2 or 3" % len(choice) - ) + ) else: store((choice, choice, choice)) @@ -132,7 +132,7 @@ class Choices(object): return '%s(%s)' % ( self.__class__.__name__, ', '.join(("%s" % repr(i) for i in self._triples)) - ) + ) def __contains__(self, item): return item in self._db_values diff --git a/model_utils/managers.py b/model_utils/managers.py index 98a2a54..b760ffd 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -76,7 +76,7 @@ class InheritanceQuerySetMixin(object): raise ValueError( '%r is not in the discovered subclasses, tried: %s' % ( subclass, ', '.join(calculated_subclasses)) - ) + ) subclasses = verified_subclasses # workaround https://code.djangoproject.com/ticket/16855 @@ -159,7 +159,7 @@ class InheritanceQuerySetMixin(object): if isinstance(rel.field, OneToOneField) and issubclass(rel.field.model, model) and model is not rel.field.model - ] + ] subclasses = [] if levels: diff --git a/tests/test_fields/test_status_field.py b/tests/test_fields/test_status_field.py index 5f077da..dc0f223 100644 --- a/tests/test_fields/test_status_field.py +++ b/tests/test_fields/test_status_field.py @@ -6,7 +6,7 @@ from model_utils.fields import StatusField from tests.models import ( Article, StatusFieldDefaultFilled, StatusFieldDefaultNotFilled, StatusFieldChoicesName, - ) +) class StatusFieldTests(TestCase): From 9189d6099682f8d81ce153b3e939421f70f6ec91 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Mon, 2 Jul 2018 13:01:49 -0700 Subject: [PATCH 209/271] Fix remaining non-E501 line length changes. The E402 module level import appears to be an error of some kind in flake8? --- tests/test_choices.py | 2 +- .../test_managers/test_inheritance_manager.py | 32 +++++++++---------- tox.ini | 1 + 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/tests/test_choices.py b/tests/test_choices.py index a503405..29dbb36 100644 --- a/tests/test_choices.py +++ b/tests/test_choices.py @@ -79,7 +79,7 @@ class LabelChoicesTests(ChoicesTests): ('DRAFT', 'is draft'), ('PUBLISHED', 'is published'), ('DELETED', 'DELETED')) - ) + ) def test_indexing(self): self.assertEqual(self.STATUS['PUBLISHED'], 'is published') diff --git a/tests/test_managers/test_inheritance_manager.py b/tests/test_managers/test_inheritance_manager.py index 39c694e..7cde8d3 100644 --- a/tests/test_managers/test_inheritance_manager.py +++ b/tests/test_managers/test_inheritance_manager.py @@ -6,11 +6,12 @@ import django from django.db import models from django.test import TestCase -from tests.models import (InheritanceManagerTestRelated, InheritanceManagerTestGrandChild1, - InheritanceManagerTestGrandChild1_2, InheritanceManagerTestParent, - InheritanceManagerTestChild1, - InheritanceManagerTestChild2, TimeFrame, InheritanceManagerTestChild3 - ) +from tests.models import ( + InheritanceManagerTestRelated, InheritanceManagerTestGrandChild1, + InheritanceManagerTestGrandChild1_2, InheritanceManagerTestParent, + InheritanceManagerTestChild1, + InheritanceManagerTestChild2, TimeFrame, InheritanceManagerTestChild3 +) class InheritanceManagerTests(TestCase): @@ -177,27 +178,26 @@ class InheritanceManagerTests(TestCase): # No argument to select_subclasses objs_1 = list( - self.get_manager(). - select_subclasses(). - values_list('id') + self.get_manager() + .select_subclasses() + .values_list('id') ) # String argument to select_subclasses objs_2 = list( - self.get_manager(). - select_subclasses( + self.get_manager() + .select_subclasses( "inheritancemanagertestchild2" - ). - values_list('id') + ) + .values_list('id') ) # String argument to select_subclasses objs_3 = list( - self.get_manager(). - select_subclasses( + self.get_manager() + .select_subclasses( InheritanceManagerTestChild2 - ). - values_list('id') + ).values_list('id') ) assert all(( diff --git a/tox.ini b/tox.ini index c46b78a..339c671 100644 --- a/tox.ini +++ b/tox.ini @@ -40,3 +40,4 @@ commands = ignore = E731, ; do not assign a lambda expression, use a def W503 ; line break before binary operator + E402 ; module level import not at top of file From c53b19e50d35ec5e14b552074af817cb7ad69b97 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Mon, 2 Jul 2018 14:33:47 -0700 Subject: [PATCH 210/271] Ignore 80 character line length restriction of pep8. It seems like this hasn't been consistently followed in this codebase, and the number of changes was fairly large. --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 339c671..71caa88 100644 --- a/tox.ini +++ b/tox.ini @@ -38,6 +38,7 @@ commands = [flake8] ignore = - E731, ; do not assign a lambda expression, use a def + E731 ; do not assign a lambda expression, use a def W503 ; line break before binary operator E402 ; module level import not at top of file + E501 ; line too long From df8ceed2650c58a00273de4a9eb5a1539ffa436c Mon Sep 17 00:00:00 2001 From: Bo Marchman Date: Tue, 7 Aug 2018 11:02:41 -0400 Subject: [PATCH 211/271] Add Django 2.1 to tox --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 71caa88..7fd9cca 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,8 @@ envlist = py27-django{18,19,110,111} py34-django{18,19,110,111,200} - py35-django{18,19,110,111,200,trunk} - py36-django{111,200,trunk} + py35-django{18,19,110,111,200,201,trunk} + py36-django{111,200,201,trunk} flake8 [testenv] @@ -13,6 +13,7 @@ deps = django110: Django>=1.10,<1.11 django111: Django>=1.11,<1.12 django200: Django>=2.0,<2.1 + django201: Django>=2.1,<2.2 djangotrunk: https://github.com/django/django/archive/master.tar.gz freezegun == 0.3.8 -rrequirements-test.txt From 4562da4e18b2cf00379e22950bdb3cfd2b8f1e4c Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Mon, 24 Sep 2018 19:39:33 +0600 Subject: [PATCH 212/271] updated doc about supported python and django versions. --- docs/setup.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/setup.rst b/docs/setup.rst index 1335bc5..1fca10c 100644 --- a/docs/setup.rst +++ b/docs/setup.rst @@ -17,7 +17,7 @@ modify your ``INSTALLED_APPS`` setting. Dependencies ============ -``django-model-utils`` supports `Django`_ 1.8 through 2.0 (latest bugfix -release in each series only) on Python 2.7, 3.3 (Django 1.8 only), 3.4 and 3.5. +``django-model-utils`` supports `Django`_ 1.8 through 2.1 (latest bugfix +release in each series only) on Python 2.7, 3.4, 3.5 and 3.6. .. _Django: http://www.djangoproject.com/ From 3efac688ed857c433dcd9225dbe832d8a7470a2b Mon Sep 17 00:00:00 2001 From: Ant Somers Date: Fri, 5 Oct 2018 02:43:52 +0300 Subject: [PATCH 213/271] Change supported version to 2.1 at README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 3014eb0..b814c47 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ django-model-utils Django model mixins and utilities. -``django-model-utils`` supports `Django`_ 1.8 to 2.0. +``django-model-utils`` supports `Django`_ 1.8 to 2.1. .. _Django: http://www.djangoproject.com/ From 300209007bf0bdfc3cfa52360880e95db5f08f80 Mon Sep 17 00:00:00 2001 From: Zach Cheung Date: Tue, 23 Oct 2018 15:38:04 +0800 Subject: [PATCH 214/271] add Simplified Chinese translations --- AUTHORS.rst | 1 + CHANGES.rst | 1 + .../locale/zh_Hans/LC_MESSAGES/django.mo | Bin 0 -> 665 bytes .../locale/zh_Hans/LC_MESSAGES/django.po | 41 ++++++++++++++++++ 4 files changed, 43 insertions(+) create mode 100644 model_utils/locale/zh_Hans/LC_MESSAGES/django.mo create mode 100644 model_utils/locale/zh_Hans/LC_MESSAGES/django.po diff --git a/AUTHORS.rst b/AUTHORS.rst index 1aea33c..8193d32 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -46,3 +46,4 @@ | Radosław Jan Ganczarek | Lucas Wiman | Jack Cushman +| Zach Cheung diff --git a/CHANGES.rst b/CHANGES.rst index 85cf4cb..eae6f8e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,7 @@ master (unreleased) - Fix handling of deferred attributes on Django 1.10+, fixes GH-278 - Fix `FieldTracker.has_changed()` and `FieldTracker.previous()` to return correct responses for deferred fields. +- Add Simplified Chinese translations. 3.1.2 (2018.05.09) ------------------ diff --git a/model_utils/locale/zh_Hans/LC_MESSAGES/django.mo b/model_utils/locale/zh_Hans/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..6766cc5be1a0a6cf4c701d842693cd35fe4df46f GIT binary patch literal 665 zcmZvYzi-n(9K{WkA2Jm~XNKF+1v{uHatNZB6h&>?D3}Vdz+qnoe^}QEC>M` zpbiKzvN55GQY6H{%EX4mL{h~1Q9q6o&mc) z0DHCt4uLP=82AbfgKyvn_zq5hAK>Nz$N2-D1`7wrdTpY7q6%v39zbWHkDw?U-HGjg z%l5W&OaWWx#ELV+6Nih$DP)qZW-!_?W$f3KdLkt5F_Ge|qvUNK8`?_f3RgOlA|Rc) zaAEFpldLqmS6a(UZkP9^GPJGJETI?6RMSWX#O*9cG_?ItL@%LjK&qZ!qrOM0XNW%= z_|;Qh&GQhXU4A#Sm+zaao*m~FnKm?1Oz4~$sRDAH#XS=CcqvkH?oO$sl9e?(_=U7! zSzd`{ao$~oFImcI#94u{c6~tBdN;x)x7}(t_x{bVcy1_#;R3Dh_c62K_sr>j&N5NA zuc9w~vXcVkaBhwBfWoss+#`wJu@!vz2U literal 0 HcmV?d00001 diff --git a/model_utils/locale/zh_Hans/LC_MESSAGES/django.po b/model_utils/locale/zh_Hans/LC_MESSAGES/django.po new file mode 100644 index 0000000..5b132f7 --- /dev/null +++ b/model_utils/locale/zh_Hans/LC_MESSAGES/django.po @@ -0,0 +1,41 @@ +# This file is distributed under the same license as the django-model-utils package. +# +# Translators: +# Zach Cheung , 2018. +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-10-23 15:12+0800\n" +"PO-Revision-Date: 2018-10-23 15:26+0800\n" +"Last-Translator: Zach Cheung \n" +"Language-Team: \n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: models.py:24 +msgid "created" +msgstr "创建时间" + +#: models.py:25 +msgid "modified" +msgstr "修改时间" + +#: models.py:37 +msgid "start" +msgstr "开始时间" + +#: models.py:38 +msgid "end" +msgstr "结束时间" + +#: models.py:53 +msgid "status" +msgstr "状态" + +#: models.py:54 +msgid "status changed" +msgstr "状态修改时间" From 05671695bb347fea57fcf33c2f0897d918184819 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Sat, 3 Nov 2018 11:12:50 +0600 Subject: [PATCH 215/271] update tag --- model_utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 54f1b02..2fa87c0 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices # noqa:F401 from .tracker import FieldTracker, ModelTracker # noqa:F401 -__version__ = '3.1.2' +__version__ = '3.2.0' From ba83be0b43f9e137ae56762f509ea9a35592ba90 Mon Sep 17 00:00:00 2001 From: tumb1er Date: Tue, 20 Nov 2018 17:49:52 +0300 Subject: [PATCH 216/271] fix #330 patch MyModel.save instead of MyModel().save --- CHANGES.rst | 1 + model_utils/tracker.py | 12 ++++++------ tests/settings.py | 6 ++++++ tests/test_fields/test_field_tracker.py | 12 +++++++++++- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 85cf4cb..7d71665 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,7 @@ master (unreleased) - Fix handling of deferred attributes on Django 1.10+, fixes GH-278 - Fix `FieldTracker.has_changed()` and `FieldTracker.previous()` to return correct responses for deferred fields. +- Fix Model instance non picklable GH-330 3.1.2 (2018.05.09) ------------------ diff --git a/model_utils/tracker.py b/model_utils/tracker.py index 8ce10dc..0059224 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -207,6 +207,7 @@ class FieldTracker(object): def contribute_to_class(self, cls, name): self.name = name self.attname = '_%s' % name + self.patch_save(cls) models.signals.class_prepared.connect(self.finalize_class, sender=cls) def finalize_class(self, sender, **kwargs): @@ -230,14 +231,13 @@ class FieldTracker(object): tracker = self.tracker_class(instance, self.fields, self.field_map) setattr(instance, self.attname, tracker) tracker.set_saved_fields() - self.patch_save(instance) instance._instance_intialized = True - def patch_save(self, instance): - original_save = instance.save + def patch_save(self, model): + original_save = model.save - def save(**kwargs): - ret = original_save(**kwargs) + def save(instance, **kwargs): + ret = original_save(instance, **kwargs) update_fields = kwargs.get('update_fields') if not update_fields and update_fields is not None: # () or [] fields = update_fields @@ -253,7 +253,7 @@ class FieldTracker(object): ) return ret - instance.save = save + model.save = save def __get__(self, instance, owner): if instance is None: diff --git a/tests/settings.py b/tests/settings.py index 8817e83..b3be03a 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -8,3 +8,9 @@ DATABASES = { } } SECRET_KEY = 'dummy' + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + } +} diff --git a/tests/test_fields/test_field_tracker.py b/tests/test_fields/test_field_tracker.py index 43f12f5..65a2bfe 100644 --- a/tests/test_fields/test_field_tracker.py +++ b/tests/test_fields/test_field_tracker.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import django from django.core.exceptions import FieldError from django.test import TestCase - +from django.core.cache import cache from model_utils import FieldTracker from model_utils.tracker import DescriptorWrapper from tests.models import ( @@ -639,6 +639,16 @@ class ModelTrackerTests(FieldTrackerTests): tracked_class = ModelTracked + def test_cache_compatible(self): + cache.set('key', self.instance) + instance = cache.get('key') + instance.number = 1 + instance.name = 'cached' + instance.save() + self.assertChanged() + instance.number = 2 + self.assertHasChanged(number=True) + def test_pre_save_changed(self): self.assertChanged() self.instance.name = 'new age' From 2cb773372dec28a6ef777663ae642a8198702cc9 Mon Sep 17 00:00:00 2001 From: Jonathan Sundqvist Date: Sun, 25 Nov 2018 15:18:23 +0100 Subject: [PATCH 217/271] Add a JoinManager that helps with performance (#351) * Add the join manager + tests * Documentation for join manager * Use order_by for consistent tests * Use postgres instead sqlite for tests for better reliability * Fix coverage * Drop django 1.8 --- .coveragerc | 5 +- .travis.yml | 2 + AUTHORS.rst | 1 + README.rst | 9 ++ docs/managers.rst | 27 ++++ model_utils/managers.py | 126 ++++++++++++++++-- requirements-test.txt | 1 + tests/models.py | 25 +++- tests/settings.py | 11 +- .../test_managers/test_inheritance_manager.py | 31 +++-- tests/test_managers/test_join_manager.py | 38 ++++++ tox.ini | 7 +- 12 files changed, 247 insertions(+), 36 deletions(-) create mode 100644 tests/test_managers/test_join_manager.py diff --git a/.coveragerc b/.coveragerc index 62d6d1c..8708371 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,5 +1,2 @@ [run] -source = model_utils -omit = .* - tests/* - */_* +include = model_utils/*.py diff --git a/.travis.yml b/.travis.yml index 295d123..a7d4622 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,8 @@ python: install: pip install tox-travis codecov # positional args ({posargs}) to pass into tox.ini script: tox -- --cov --cov-append +services: + - postgresql after_success: codecov deploy: provider: pypi diff --git a/AUTHORS.rst b/AUTHORS.rst index 1aea33c..459b755 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -22,6 +22,7 @@ | Jarek Glowacki | Javier García Sogo | Jeff Elmore +| Jonathan Sundqvist | Keryn Knight | Martey Dodoo | Matthew Schinckel diff --git a/README.rst b/README.rst index b814c47..6d74e7a 100644 --- a/README.rst +++ b/README.rst @@ -28,6 +28,15 @@ Getting Help Documentation for django-model-utils is available https://django-model-utils.readthedocs.io/ + +Run tests +--------- + +.. code-block + + pip install -e . + py.test + Contributing ============ diff --git a/docs/managers.rst b/docs/managers.rst index 43aa030..b90f3d4 100644 --- a/docs/managers.rst +++ b/docs/managers.rst @@ -86,6 +86,33 @@ 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/ +JoinManager +----------- + +The ``JoinManager`` will create a temporary table of your current queryset +and join that temporary table with the model of your current queryset. This can +be advantageous if you have to page through your entire DB and using django's +slice mechanism to do that. ``LIMIT .. OFFSET ..`` becomes slower the bigger +offset you use. + +.. code-block:: python + + sliced_qs = Place.objects.all()[2000:2010] + qs = sliced_qs.join() + # qs contains 10 objects, and there will be a much smaller performance hit + # for paging through all of first 2000 objects. + +Alternatively, you can give it a queryset and the manager will create a temporary +table and join that to your current queryset. This can work as a more performant +alternative to using django's ``__in`` as described in the following +(`StackExchange answer`_). + +.. code-block:: python + + big_qs = Restaurant.objects.filter(menu='vegetarian') + qs = Country.objects.filter(country_code='SE').join(big_qs) + +.. _StackExchange answer: https://dba.stackexchange.com/questions/91247/optimizing-a-postgres-query-with-a-large-in .. _QueryManager: diff --git a/model_utils/managers.py b/model_utils/managers.py index b760ffd..afd0c5b 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -3,16 +3,15 @@ import django from django.db import models from django.db.models.fields.related import OneToOneField, OneToOneRel from django.db.models.query import QuerySet -try: - from django.db.models.query import BaseIterable, ModelIterable -except ImportError: - # Django 1.8 does not have iterable classes - BaseIterable, ModelIterable = object, object +from django.db.models.query import ModelIterable from django.core.exceptions import ObjectDoesNotExist from django.db.models.constants import LOOKUP_SEP from django.utils.six import string_types +from django.db import connection +from django.db.models.sql.datastructures import Join + class InheritanceIterable(ModelIterable): def __iter__(self): @@ -104,10 +103,6 @@ class InheritanceQuerySetMixin(object): if hasattr(self, name): kwargs[name] = getattr(self, name) - if django.VERSION < (1, 9): - kwargs['klass'] = klass - kwargs['setup'] = setup - return super(InheritanceQuerySetMixin, self)._clone(**kwargs) def annotate(self, *args, **kwargs): @@ -189,10 +184,7 @@ class InheritanceQuerySetMixin(object): if levels: levels -= 1 while parent_link is not None: - if django.VERSION < (1, 9): - related = parent_link.rel - else: - related = parent_link.remote_field + related = parent_link.remote_field ancestry.insert(0, related.get_accessor_name()) if levels or levels is None: parent_model = related.model @@ -308,3 +300,111 @@ class SoftDeletableManagerMixin(object): class SoftDeletableManager(SoftDeletableManagerMixin, models.Manager): pass + + +class JoinQueryset(models.QuerySet): + + def get_quoted_query(self, query): + query, params = query.sql_with_params() + + # Put additional quotes around string. + params = [ + '\'{}\''.format(p) + if isinstance(p, str) else p + for p in params + ] + + # Cast list of parameters to tuple because I got + # "not enough format characters" otherwise. + params = tuple(params) + return query % params + + def join(self, qs=None): + ''' + Join one queryset together with another using a temporary table. If + no queryset is used, it will use the current queryset and join that + to itself. + + `Join` either uses the current queryset and effectively does a self-join to + create a new limited queryset OR it uses a querset given by the user. + + The model of a given queryset needs to contain a valid foreign key to + the current queryset to perform a join. A new queryset is then created. + ''' + to_field = 'id' + + if qs: + fk = [ + fk for fk in qs.model._meta.fields + if getattr(fk, 'related_model', None) == self.model + ] + fk = fk[0] if fk else None + model_set = '{}_set'.format(self.model.__name__.lower()) + key = fk or getattr(qs.model, model_set, None) + + if not key: + raise ValueError('QuerySet is not related to current model') + + try: + fk_column = key.column + except AttributeError: + fk_column = 'id' + to_field = key.field.column + + qs = qs.only(fk_column) + # if we give a qs we need to keep the model qs to not lose anything + new_qs = self + else: + fk_column = 'id' + qs = self.only(fk_column) + new_qs = self.model.objects.all() + + TABLE_NAME = 'temp_stuff' + query = self.get_quoted_query(qs.query) + sql = ''' + DROP TABLE IF EXISTS {table_name}; + DROP INDEX IF EXISTS {table_name}_id; + CREATE TEMPORARY TABLE {table_name} AS {query}; + CREATE INDEX {table_name}_{fk_column} ON {table_name} ({fk_column}); + '''.format(table_name=TABLE_NAME, fk_column=fk_column, query=str(query)) + + with connection.cursor() as cursor: + cursor.execute(sql) + + class TempModel(models.Model): + temp_key = models.ForeignKey( + self.model, + on_delete=models.DO_NOTHING, + db_column=fk_column, + to_field=to_field + ) + + class Meta: + managed = False + db_table = TABLE_NAME + + conn = Join( + table_name=TempModel._meta.db_table, + parent_alias=new_qs.query.get_initial_alias(), + table_alias=None, + join_type='INNER JOIN', + join_field=self.model.tempmodel_set.rel, + nullable=False + ) + new_qs.query.join(conn, reuse=None) + return new_qs + + +class JoinManagerMixin(object): + """ + Manager that adds a method join. This method allows you to join two + querysets together. + """ + _queryset_class = JoinQueryset + + def get_queryset(self): + return self._queryset_class(model=self.model, using=self._db) + + +class JoinManager(JoinManagerMixin, models.Manager): + pass diff --git a/requirements-test.txt b/requirements-test.txt index 493f267..fa21abd 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,2 +1,3 @@ pytest==3.3.1 pytest-django==3.1.2 +psycopg2==2.7.6.1 diff --git a/tests/models.py b/tests/models.py index 80f3a3f..888aba5 100644 --- a/tests/models.py +++ b/tests/models.py @@ -9,7 +9,11 @@ from django.utils.translation import ugettext_lazy as _ from model_utils import Choices from model_utils.fields import SplitField, MonitorField, StatusField -from model_utils.managers import QueryManager, InheritanceManager +from model_utils.managers import ( + QueryManager, + InheritanceManager, + JoinManagerMixin +) from model_utils.models import ( SoftDeletableModel, StatusModel, @@ -370,3 +374,22 @@ class ModelWithCustomDescriptor(models.Model): tracked_regular_field = models.IntegerField() tracker = FieldTracker(fields=['tracked_custom_field', 'tracked_regular_field']) + + +class JoinManager(JoinManagerMixin, models.Manager): + pass + + +class BoxJoinModel(models.Model): + name = models.CharField(max_length=32) + objects = JoinManager() + + +class JoinItemForeignKey(models.Model): + weight = models.IntegerField() + belonging = models.ForeignKey( + BoxJoinModel, + null=True, + on_delete=models.CASCADE + ) + objects = JoinManager() diff --git a/tests/settings.py b/tests/settings.py index b3be03a..a8d231c 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,11 +1,16 @@ +import os + INSTALLED_APPS = ( 'model_utils', 'tests', ) DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3' - } + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": os.environ.get("DJANGO_DATABASE_NAME_POSTGRES", "modelutils"), + "USER": os.environ.get("DJANGO_DATABASE_USER_POSTGRES", 'postgres'), + "PASSWORD": os.environ.get("DJANGO_DATABASE_PASSWORD_POSTGRES", ""), + }, } SECRET_KEY = 'dummy' diff --git a/tests/test_managers/test_inheritance_manager.py b/tests/test_managers/test_inheritance_manager.py index 7cde8d3..14ae047 100644 --- a/tests/test_managers/test_inheritance_manager.py +++ b/tests/test_managers/test_inheritance_manager.py @@ -123,10 +123,16 @@ class InheritanceManagerTests(TestCase): ensure that the relation names and subclasses are obtained correctly. """ child3 = InheritanceManagerTestChild3.objects.create() - results = InheritanceManagerTestParent.objects.all().select_subclasses() + qs = InheritanceManagerTestParent.objects.all() + results = qs.select_subclasses().order_by('pk') - expected_objs = [self.child1, self.child2, self.grandchild1, - self.grandchild1_2, child3] + expected_objs = [ + self.child1, + self.child2, + self.grandchild1, + self.grandchild1_2, + child3 + ] self.assertEqual(list(results), expected_objs) expected_related_names = [ @@ -146,7 +152,8 @@ class InheritanceManagerTests(TestCase): """ related_name = 'manual_onetoone' child3 = InheritanceManagerTestChild3.objects.create() - results = InheritanceManagerTestParent.objects.all().select_subclasses(related_name) + qs = InheritanceManagerTestParent.objects.all() + results = qs.select_subclasses(related_name).order_by('pk') expected_objs = [InheritanceManagerTestParent(pk=self.child1.pk), InheritanceManagerTestParent(pk=self.child2.pk), @@ -389,14 +396,16 @@ class InheritanceManagerUsingModelsTests(TestCase): """ child3 = InheritanceManagerTestChild3.objects.create() results = InheritanceManagerTestParent.objects.all().select_subclasses( - InheritanceManagerTestChild3) + InheritanceManagerTestChild3).order_by('pk') - expected_objs = [InheritanceManagerTestParent(pk=self.parent1.pk), - InheritanceManagerTestParent(pk=self.child1.pk), - InheritanceManagerTestParent(pk=self.child2.pk), - InheritanceManagerTestParent(pk=self.grandchild1.pk), - InheritanceManagerTestParent(pk=self.grandchild1_2.pk), - child3] + expected_objs = [ + InheritanceManagerTestParent(pk=self.parent1.pk), + InheritanceManagerTestParent(pk=self.child1.pk), + InheritanceManagerTestParent(pk=self.child2.pk), + InheritanceManagerTestParent(pk=self.grandchild1.pk), + InheritanceManagerTestParent(pk=self.grandchild1_2.pk), + child3 + ] self.assertEqual(list(results), expected_objs) expected_related_names = ['manual_onetoone'] diff --git a/tests/test_managers/test_join_manager.py b/tests/test_managers/test_join_manager.py new file mode 100644 index 0000000..b8a8131 --- /dev/null +++ b/tests/test_managers/test_join_manager.py @@ -0,0 +1,38 @@ + +from django.test import TestCase + +from tests.models import JoinItemForeignKey, BoxJoinModel + + +class JoinManagerTest(TestCase): + def setUp(self): + for i in range(20): + BoxJoinModel.objects.create(name='name_{i}'.format(i=i)) + + JoinItemForeignKey.objects.create( + weight=10, belonging=BoxJoinModel.objects.get(name='name_1') + ) + JoinItemForeignKey.objects.create(weight=20) + + def test_self_join(self): + a_slice = BoxJoinModel.objects.all()[0:10] + with self.assertNumQueries(1): + result = a_slice.join() + self.assertEquals(result.count(), 10) + + def test_self_join_with_where_statement(self): + qs = BoxJoinModel.objects.filter(name='name_1') + result = qs.join() + self.assertEquals(result.count(), 1) + + def test_join_with_other_qs(self): + item_qs = JoinItemForeignKey.objects.filter(weight=10) + boxes = BoxJoinModel.objects.all().join(qs=item_qs) + self.assertEquals(boxes.count(), 1) + self.assertEquals(boxes[0].name, 'name_1') + + def test_reverse_join(self): + box_qs = BoxJoinModel.objects.filter(name='name_1') + items = JoinItemForeignKey.objects.all().join(box_qs) + self.assertEquals(items.count(), 1) + self.assertEquals(items[0].weight, 10) diff --git a/tox.ini b/tox.ini index 7fd9cca..c092160 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,13 @@ [tox] envlist = - py27-django{18,19,110,111} - py34-django{18,19,110,111,200} - py35-django{18,19,110,111,200,201,trunk} + py27-django{19,110,111} + py34-django{19,110,111,200} + py35-django{19,110,111,200,201,trunk} py36-django{111,200,201,trunk} flake8 [testenv] deps = - django18: Django>=1.8,<1.9 django19: Django>=1.9,<1.10 django110: Django>=1.10,<1.11 django111: Django>=1.11,<1.12 From 326bd7bc02f59846af74d53e9da1394f81b894df Mon Sep 17 00:00:00 2001 From: Sebastian Illing Date: Sat, 8 Dec 2018 06:26:36 +0100 Subject: [PATCH 218/271] Fix missing subclasses and annotated in cloned InheritanceQueryset (#335) Bug only occurs in django > 2 --- model_utils/managers.py | 6 +++++- tests/test_managers/test_inheritance_manager.py | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/model_utils/managers.py b/model_utils/managers.py index afd0c5b..15b5c7c 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -97,7 +97,11 @@ class InheritanceQuerySetMixin(object): def _clone(self, klass=None, setup=False, **kwargs): if django.VERSION >= (2, 0): - return super(InheritanceQuerySetMixin, self)._clone() + qs = super(InheritanceQuerySetMixin, self)._clone() + for name in ['subclasses', '_annotated']: + if hasattr(self, name): + setattr(qs, name, getattr(self, name)) + return qs for name in ['subclasses', '_annotated']: if hasattr(self, name): diff --git a/tests/test_managers/test_inheritance_manager.py b/tests/test_managers/test_inheritance_manager.py index 14ae047..d2b8b4f 100644 --- a/tests/test_managers/test_inheritance_manager.py +++ b/tests/test_managers/test_inheritance_manager.py @@ -460,3 +460,7 @@ class InheritanceManagerRelatedTests(InheritanceManagerTests): qs = InheritanceManagerTestParent.objects.annotate( test_count=models.Count('id')).select_subclasses() self.assertEqual(qs.get(id=self.child1.id).test_count, 1) + + def test_clone_when_inheritance_queryset_selects_subclasses_should_clone_them_too(self): + qs = InheritanceManagerTestParent.objects.select_subclasses() + self.assertEqual(qs.subclasses, qs._clone().subclasses) From 764b7ea78d506cf06828e4034946d86afa67770a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Araujo?= Date: Sat, 8 Dec 2018 00:47:54 -0500 Subject: [PATCH 219/271] Add support for reverse iteration of Choices (#314) --- CHANGES.rst | 1 + model_utils/choices.py | 3 +++ tests/test_choices.py | 28 ++++++++++++++++++++++++---- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7d71665..4eff3da 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,7 @@ master (unreleased) - Fix handling of deferred attributes on Django 1.10+, fixes GH-278 - Fix `FieldTracker.has_changed()` and `FieldTracker.previous()` to return correct responses for deferred fields. +- Support `reversed` for all kinds of `Choices` objects, fixes GH-309 - Fix Model instance non picklable GH-330 3.1.2 (2018.05.09) diff --git a/model_utils/choices.py b/model_utils/choices.py index 681e44b..6339503 100644 --- a/model_utils/choices.py +++ b/model_utils/choices.py @@ -102,6 +102,9 @@ class Choices(object): def __iter__(self): return iter(self._doubles) + def __reversed__(self): + return reversed(self._doubles) + def __getattr__(self, attname): try: return self._identifier_map[attname] diff --git a/tests/test_choices.py b/tests/test_choices.py index 29dbb36..986670c 100644 --- a/tests/test_choices.py +++ b/tests/test_choices.py @@ -16,7 +16,12 @@ class ChoicesTests(TestCase): self.assertEqual(self.STATUS['PUBLISHED'], 'PUBLISHED') def test_iteration(self): - self.assertEqual(tuple(self.STATUS), (('DRAFT', 'DRAFT'), ('PUBLISHED', 'PUBLISHED'))) + self.assertEqual(tuple(self.STATUS), + (('DRAFT', 'DRAFT'), ('PUBLISHED', 'PUBLISHED'))) + + def test_reversed(self): + self.assertEqual(tuple(reversed(self.STATUS)), + (('PUBLISHED', 'PUBLISHED'), ('DRAFT', 'DRAFT'))) def test_len(self): self.assertEqual(len(self.STATUS), 2) @@ -78,8 +83,15 @@ class LabelChoicesTests(ChoicesTests): self.assertEqual(tuple(self.STATUS), ( ('DRAFT', 'is draft'), ('PUBLISHED', 'is published'), - ('DELETED', 'DELETED')) - ) + ('DELETED', 'DELETED'), + )) + + def test_reversed(self): + self.assertEqual(tuple(reversed(self.STATUS)), ( + ('DELETED', 'DELETED'), + ('PUBLISHED', 'is published'), + ('DRAFT', 'is draft'), + )) def test_indexing(self): self.assertEqual(self.STATUS['PUBLISHED'], 'is published') @@ -169,7 +181,15 @@ class IdentifierChoicesTests(ChoicesTests): self.assertEqual(tuple(self.STATUS), ( (0, 'is draft'), (1, 'is published'), - (2, 'is deleted'))) + (2, 'is deleted'), + )) + + def test_reversed(self): + self.assertEqual(tuple(reversed(self.STATUS)), ( + (2, 'is deleted'), + (1, 'is published'), + (0, 'is draft'), + )) def test_indexing(self): self.assertEqual(self.STATUS[1], 'is published') From b739f6fe87257ac93d43ea32c2bfbb7230593c41 Mon Sep 17 00:00:00 2001 From: Guilherme Devincenzi Date: Sat, 8 Dec 2018 03:50:10 -0200 Subject: [PATCH 220/271] Add default manager as all_objects for SoftDeletableModel (#326) * Add default manager as all_objects for SoftDeletableModel * Document changes on changelog --- CHANGES.rst | 3 +++ model_utils/models.py | 1 + 2 files changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4eff3da..cdc681d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,9 @@ master (unreleased) * Update InheritanceIterable to inherit from ModelIterable instead of BaseIterable, fixes GH-277. +* Add all_objects Manager for 'SoftDeletableModel' to include soft + deleted objects on queries as per issue GH-255 + 3.1.1 (2017.12.17) ------------------ diff --git a/model_utils/models.py b/model_utils/models.py index c679fc6..2f21695 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -123,6 +123,7 @@ class SoftDeletableModel(models.Model): abstract = True objects = SoftDeletableManager() + all_objects = models.Manager() def delete(self, using=None, soft=True, *args, **kwargs): """ From 25743141bccfda81b04dd1da7d8f24733a9471f8 Mon Sep 17 00:00:00 2001 From: Daniel Andrlik Date: Sat, 8 Dec 2018 01:23:04 -0500 Subject: [PATCH 221/271] Ensure TimeStampedModel modified equals created on initial creation. (#319) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✅ Ensure TimeStampedModel modified equals created on initial creation. Add logic to AutoLastModifiedField which checks to see if the associated created field of the correct cField class type is also present. If the instance has not yet been saved (missing a pk), then set the value to modified to be equal to created. Fixes #247 📚 Update changes and authors list related to changes. * 🚑 Set TimeStampedModel modified to be equal to created during first save. If instance does not yet have a pk, before defaulting the last modified to the current time, iterate over the the fields of the model, and instead use whatever value is found in the first occurance of the AutoCreatedField. Fixes #247 * Move changelog up to unreleased section. --- AUTHORS.rst | 1 + CHANGES.rst | 2 ++ model_utils/fields.py | 5 +++++ tests/test_models/test_timestamped_model.py | 7 +++++++ 4 files changed, 15 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 459b755..8d29344 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -47,3 +47,4 @@ | Radosław Jan Ganczarek | Lucas Wiman | Jack Cushman +| Daniel Andrlik diff --git a/CHANGES.rst b/CHANGES.rst index cdc681d..e37c172 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,8 @@ master (unreleased) - Fix handling of deferred attributes on Django 1.10+, fixes GH-278 - Fix `FieldTracker.has_changed()` and `FieldTracker.previous()` to return correct responses for deferred fields. +- Update AutoLastModifiedField so that at instance creation it will + always be set equal to created to make querying easier. Fixes GH-254 - Support `reversed` for all kinds of `Choices` objects, fixes GH-309 - Fix Model instance non picklable GH-330 diff --git a/model_utils/fields.py b/model_utils/fields.py index d9d9b85..2799eac 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -32,6 +32,11 @@ class AutoLastModifiedField(AutoCreatedField): """ def pre_save(self, model_instance, add): value = now() + if not model_instance.pk: + for field in model_instance._meta.get_fields(): + if isinstance(field, AutoCreatedField): + value = getattr(model_instance, field.name) + break setattr(model_instance, self.attname, value) return value diff --git a/tests/test_models/test_timestamped_model.py b/tests/test_models/test_timestamped_model.py index 8760411..cac07f3 100644 --- a/tests/test_models/test_timestamped_model.py +++ b/tests/test_models/test_timestamped_model.py @@ -15,6 +15,13 @@ class TimeStampedModelTests(TestCase): t1 = TimeStamp.objects.create() self.assertEqual(t1.created, datetime(2016, 1, 1)) + def test_created_sets_modified(self): + ''' + Ensure that on creation that modifed is set exactly equal to created. + ''' + t1 = TimeStamp.objects.create() + self.assertEqual(t1.created, t1.modified) + def test_modified(self): with freeze_time(datetime(2016, 1, 1)): t1 = TimeStamp.objects.create() From 6b88c888d3e39aa1ebd72f834d08f44dd6997515 Mon Sep 17 00:00:00 2001 From: Sergey Tikhonov Date: Mon, 10 Dec 2018 18:35:26 +0300 Subject: [PATCH 222/271] fix model.save patched in FieldTracker (#353) * fix model.save patched in FieldTracker * add test for save call with args * update changes --- CHANGES.rst | 1 + model_utils/tracker.py | 4 ++-- tests/settings.py | 1 + tests/test_fields/test_field_tracker.py | 5 +++++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e37c172..7e6039d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,7 @@ master (unreleased) always be set equal to created to make querying easier. Fixes GH-254 - Support `reversed` for all kinds of `Choices` objects, fixes GH-309 - Fix Model instance non picklable GH-330 +- Fix patched `save` in FieldTracker 3.1.2 (2018.05.09) ------------------ diff --git a/model_utils/tracker.py b/model_utils/tracker.py index 0059224..837e1ce 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -236,8 +236,8 @@ class FieldTracker(object): def patch_save(self, model): original_save = model.save - def save(instance, **kwargs): - ret = original_save(instance, **kwargs) + def save(instance, *args, **kwargs): + ret = original_save(instance, *args, **kwargs) update_fields = kwargs.get('update_fields') if not update_fields and update_fields is not None: # () or [] fields = update_fields diff --git a/tests/settings.py b/tests/settings.py index a8d231c..e34c891 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -10,6 +10,7 @@ DATABASES = { "NAME": os.environ.get("DJANGO_DATABASE_NAME_POSTGRES", "modelutils"), "USER": os.environ.get("DJANGO_DATABASE_USER_POSTGRES", 'postgres'), "PASSWORD": os.environ.get("DJANGO_DATABASE_PASSWORD_POSTGRES", ""), + "HOST": os.environ.get("DJANGO_DATABASE_HOST_POSTGRES", ""), }, } SECRET_KEY = 'dummy' diff --git a/tests/test_fields/test_field_tracker.py b/tests/test_fields/test_field_tracker.py index 65a2bfe..5b5d5c2 100644 --- a/tests/test_fields/test_field_tracker.py +++ b/tests/test_fields/test_field_tracker.py @@ -85,6 +85,11 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): self.instance.mutable = [1, 2, 3] self.assertHasChanged(name=True, number=True, mutable=True) + def test_save_with_args(self): + self.instance.number = 1 + self.instance.save(False, False, None, None) + self.assertChanged() + def test_first_save(self): self.assertHasChanged(name=True, number=False, mutable=False) self.assertPrevious(name=None, number=None, mutable=None) From 4d9df911b342707d7cf6cf9143ba5285c5773d1d Mon Sep 17 00:00:00 2001 From: Reece Dunham Date: Thu, 10 Jan 2019 15:38:56 -0500 Subject: [PATCH 223/271] Update year. --- LICENSE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.txt b/LICENSE.txt index 0eadf47..01e3613 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2009-2015, Carl Meyer and contributors +Copyright (c) 2009-2019, Carl Meyer and contributors All rights reserved. Redistribution and use in source and binary forms, with or without From 3be02d30015268aa0f2986fa6a6a18f9f7e577a5 Mon Sep 17 00:00:00 2001 From: Reece Dunham Date: Fri, 11 Jan 2019 10:04:43 -0500 Subject: [PATCH 224/271] Update AUTHORS.rst --- AUTHORS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index daa715b..06a3e01 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -15,6 +15,7 @@ | Felipe Prenholato | Filipe Ximenes | Gregor Müllegger +| Germano Massullo | Hanley Hansen | ivirabyan | James Oakley From c1bcd67971851afc77b3092de0ae5fda25901905 Mon Sep 17 00:00:00 2001 From: Reece Dunham Date: Fri, 11 Jan 2019 10:09:34 -0500 Subject: [PATCH 225/271] Revert "Add missing classifiers in setup.py" From 4e187105375a8cc090671df16c5ffa7b7e84501f Mon Sep 17 00:00:00 2001 From: Diego Navarro Date: Thu, 21 Feb 2019 19:05:35 +0100 Subject: [PATCH 226/271] Upgrades pytest, pytest-django and pytest-cov version to fix travis build (#358) * Upgrades pytest, pytest-django and pytest-cov version * Skips testing Python 3.5 with django trunk (Django 2.1 requires Python 3.6) --- CHANGES.rst | 2 ++ requirements-test.txt | 5 +++-- tox.ini | 3 +-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4ea6812..629c4dc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,6 +12,8 @@ master (unreleased) - Support `reversed` for all kinds of `Choices` objects, fixes GH-309 - Fix Model instance non picklable GH-330 - Fix patched `save` in FieldTracker +- Upgrades test requirements (pytest, pytest-django, pytest-cov) and + skips tox test with Python 3.5 and Django (trunk) 3.1.2 (2018.05.09) ------------------ diff --git a/requirements-test.txt b/requirements-test.txt index fa21abd..f8c6741 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,3 +1,4 @@ -pytest==3.3.1 -pytest-django==3.1.2 +pytest==4.3.0 +pytest-django==3.4.7 psycopg2==2.7.6.1 +pytest-cov==2.6.1 \ No newline at end of file diff --git a/tox.ini b/tox.ini index c092160..4cdf852 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ envlist = py27-django{19,110,111} py34-django{19,110,111,200} - py35-django{19,110,111,200,201,trunk} + py35-django{19,110,111,200,201} py36-django{111,200,201,trunk} flake8 @@ -16,7 +16,6 @@ deps = djangotrunk: https://github.com/django/django/archive/master.tar.gz freezegun == 0.3.8 -rrequirements-test.txt - pytest-cov ignore_outcome = djangotrunk: True passenv = From 38f8932e7413cde486b4f4dd20d81437a67f9b2b Mon Sep 17 00:00:00 2001 From: JMP Date: Tue, 26 Feb 2019 17:32:56 +0100 Subject: [PATCH 227/271] UUID model added --- model_utils/models.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/model_utils/models.py b/model_utils/models.py index 2f21695..0cfafe6 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -1,5 +1,8 @@ from __future__ import unicode_literals +from model_utils.managers import QueryManager, SoftDeletableManager +from model_utils.fields import AutoCreatedField, AutoLastModifiedField, StatusField, MonitorField, UUIDField + import django from django.core.exceptions import ImproperlyConfigured from django.db import models @@ -10,10 +13,6 @@ if django.VERSION >= (1, 9, 0): else: from django.utils.timezone import now -from model_utils.managers import QueryManager, SoftDeletableManager -from model_utils.fields import AutoCreatedField, AutoLastModifiedField, \ - StatusField, MonitorField - class TimeStampedModel(models.Model): """ @@ -135,3 +134,18 @@ class SoftDeletableModel(models.Model): self.save(using=using) else: return super(SoftDeletableModel, self).delete(using=using, *args, **kwargs) + + +class UUIDModel(models.Model): + """ + This abstract base class provides id field on any model that inherits from it + which will be the primary key. + """ + id = UUIDField( + primary_key=True, + version=4, + editable=False, + ) + + class Meta: + abstract = True From ca752948835039bea5a86215fd478beb58df8ab8 Mon Sep 17 00:00:00 2001 From: JMP Date: Tue, 26 Feb 2019 17:33:06 +0100 Subject: [PATCH 228/271] UUID field added --- model_utils/fields.py | 60 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/model_utils/fields.py b/model_utils/fields.py index 2799eac..5bb630e 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -1,11 +1,19 @@ from __future__ import unicode_literals import django +try: + import uuid # noqa + HAS_UUID = True +except ImportError: + HAS_UUID = False from django.db import models from django.conf import settings +from django.core.exceptions import ImproperlyConfigured from django.utils.encoding import python_2_unicode_compatible from django.utils.timezone import now +from model_utils.exceptions import UUIDVersionException + DEFAULT_CHOICES_NAME = 'STATUS' @@ -17,6 +25,7 @@ class AutoCreatedField(models.DateTimeField): By default, sets editable=False, default=datetime.now. """ + def __init__(self, *args, **kwargs): kwargs.setdefault('editable', False) kwargs.setdefault('default', now) @@ -30,6 +39,7 @@ class AutoLastModifiedField(AutoCreatedField): By default, sets editable=False and default=datetime.now. """ + def pre_save(self, model_instance, add): value = now() if not model_instance.pk: @@ -53,6 +63,7 @@ class StatusField(models.CharField): Also features a ``no_check_for_status`` argument to make sure South can handle this field when it freezes a model. """ + def __init__(self, *args, **kwargs): kwargs.setdefault('max_length', 100) self.check_for_status = not kwargs.pop('no_check_for_status', False) @@ -93,6 +104,7 @@ class MonitorField(models.DateTimeField): changes. """ + def __init__(self, *args, **kwargs): kwargs.setdefault('default', now) monitor = kwargs.pop('monitor', None) @@ -144,7 +156,8 @@ SPLIT_MARKER = getattr(settings, 'SPLIT_MARKER', '') # the number of paragraphs after which to split if no marker SPLIT_DEFAULT_PARAGRAPHS = getattr(settings, 'SPLIT_DEFAULT_PARAGRAPHS', 2) -_excerpt_field_name = lambda name: '_%s_excerpt' % name + +def _excerpt_field_name(name): return '_%s_excerpt' % name def get_excerpt(content): @@ -252,3 +265,48 @@ class SplitField(models.TextField): name, path, args, kwargs = super(SplitField, self).deconstruct() kwargs['no_excerpt_field'] = True return name, path, args, kwargs + + +class UUIDField(models.UUIDField): + """ + A field for storing universally unique identifiers. Uses Python’s UUID class. + """ + + def __init__(self, primary_key=True, version=4, editable=False, *args, **kwargs): + """ + Parameters + ---------- + primary_key : bool + If True, this field is the primary key for the model. + version : int + An integer that set default UUID version. + editable : bool + If False, the field will not be displayed in the admin or any other ModelForm, + default is false. + + Raises + ------ + UUIDVersionException + UUID version 2 is not supported. + """ + if not HAS_UUID: + raise ImproperlyConfigured("'uuid' module is required for UUIDField.") + + kwargs.setdefault('primary_key', primary_key) + kwargs.setdefault('editable', editable) + + if version == 4: + default = uuid.uuid4 + elif version == 1: + default = uuid.uuid1 + elif version == 2: + raise UUIDVersionException("UUID version 2 is not supported.") + elif version == 3: + default = uuid.uuid3 + elif version == 5: + default = uuid.uuid5 + else: + raise UUIDVersionException("UUID version %s is not valid." % version) + + kwargs.setdefault('default', default) + super(UUIDField, self).__init__(*args, **kwargs) From 5bf7db036fde92b0752aeb3869bd679465be7584 Mon Sep 17 00:00:00 2001 From: JMP Date: Tue, 26 Feb 2019 17:34:20 +0100 Subject: [PATCH 229/271] UUIDModel doc --- docs/models.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/models.rst b/docs/models.rst index 51bde8f..4e04c29 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -55,3 +55,24 @@ SoftDeletableModel This abstract base class just provides field ``is_removed`` which is set to True instead of removing the instance. Entities returned in default manager are limited to not-deleted instances. + + +UUIDModel +------------------ + +This abstract base class provides ``id`` field on any model that inherits from it +which will be the primary key. + +If you dont want to set ``id`` as primary key or change the field name, you can be override it +with our [UUIDField](https://github.com/jazzband/django-model-utils/blob/master/docs/fields.rst#uuidfield). + +Also you can override the default uuid version. Versions 1,3,4 and 5 are now supported. + +.. code-block:: python + + from model_utils.models import UUIDModel + from model_utils import Choices + + class MyAppModel(UUIDModel): + pass + From bdc6fb05fecdf9ab68ee0f6f62fdb76209b94b7a Mon Sep 17 00:00:00 2001 From: JMP Date: Tue, 26 Feb 2019 17:35:00 +0100 Subject: [PATCH 230/271] UUIDField doc --- docs/fields.rst | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/fields.rst b/docs/fields.rst index 02ca6ef..1f0d5c5 100644 --- a/docs/fields.rst +++ b/docs/fields.rst @@ -154,3 +154,29 @@ If no marker is found in the content, the first two paragraphs (where paragraphs are blocks of text separated by a blank line) are taken to be the excerpt. This number can be customized by setting the ``SPLIT_DEFAULT_PARAGRAPHS`` setting. + + +UUIDField +---------- + +A ``UUIDField``subclass that provides an UUID field. You can +add this field to any model definition. + +With the param ``primary_key`` you can set if this field is the +primary key for the model, default is True. + +Param ``version`` is an integer that set default UUID version. +Versions 1,3,4 and 5 are supported, default is 4. + +If ``editable`` is set to false the field will not be displayed in the admin +or any other ModelForm, default is False. + + +.. code-block:: python + + from django.db import models + from model_utils.fields import UUIDField + + class MyAppModel(models.Model): + uuid = UUIDField(primary_key=True, version=4, editable=False) + From 5ff0867bf92dd06ac3bc8aa4e168ae64c2c3a07f Mon Sep 17 00:00:00 2001 From: JMP Date: Tue, 26 Feb 2019 17:35:50 +0100 Subject: [PATCH 231/271] UUIDModels for testing purposes --- tests/models.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/models.py b/tests/models.py index 888aba5..72e48a4 100644 --- a/tests/models.py +++ b/tests/models.py @@ -8,7 +8,12 @@ from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from model_utils import Choices -from model_utils.fields import SplitField, MonitorField, StatusField +from model_utils.fields import ( + SplitField, + MonitorField, + StatusField, + UUIDField, +) from model_utils.managers import ( QueryManager, InheritanceManager, @@ -19,6 +24,7 @@ from model_utils.models import ( StatusModel, TimeFramedModel, TimeStampedModel, + UUIDModel, ) from tests.fields import MutableField from tests.managers import CustomSoftDeleteManager @@ -159,8 +165,8 @@ class Post(models.Model): objects = models.Manager() public = QueryManager(published=True) - public_confirmed = QueryManager(models.Q(published=True) & - models.Q(confirmed=True)) + public_confirmed = QueryManager(models.Q(published=True) + & models.Q(confirmed=True)) public_reversed = QueryManager(published=True).order_by("-order") class Meta: @@ -340,6 +346,7 @@ class StringyDescriptor(object): """ Descriptor that returns a string version of the underlying integer value. """ + def __init__(self, name): self.name = name @@ -393,3 +400,11 @@ class JoinItemForeignKey(models.Model): on_delete=models.CASCADE ) objects = JoinManager() + + +class CustomUUIDModel(UUIDModel): + pass + + +class CustomNotPrimaryUUIDModel(models.Model): + uuid = UUIDField(primary_key=False) From 58e57d55356eb9fdc3e78b38b9e38389e7798b8d Mon Sep 17 00:00:00 2001 From: JMP Date: Tue, 26 Feb 2019 17:36:23 +0100 Subject: [PATCH 232/271] UUIDField tests --- tests/test_fields/test_uuid_field.py | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/test_fields/test_uuid_field.py diff --git a/tests/test_fields/test_uuid_field.py b/tests/test_fields/test_uuid_field.py new file mode 100644 index 0000000..74faa4f --- /dev/null +++ b/tests/test_fields/test_uuid_field.py @@ -0,0 +1,34 @@ +from __future__ import unicode_literals + +import uuid + +from django.test import TestCase + +from model_utils.fields import UUIDField +from model_utils.exceptions import UUIDVersionException + + +class UUIDFieldTests(TestCase): + + def test_uuid_version_default(self): + instance = UUIDField() + self.assertEqual(instance.default, uuid.uuid4) + + def test_uuid_version_1(self): + instance = UUIDField(version=1) + self.assertEqual(instance.default, uuid.uuid1) + + def test_uuid_version_2_error(self): + self.assertRaises(UUIDVersionException, UUIDField, 'version', 2) + + def test_uuid_version_3(self): + instance = UUIDField(version=3) + self.assertEqual(instance.default, uuid.uuid3) + + def test_uuid_version_4(self): + instance = UUIDField(version=4) + self.assertEqual(instance.default, uuid.uuid4) + + def test_uuid_version_5(self): + instance = UUIDField(version=5) + self.assertEqual(instance.default, uuid.uuid5) From c23c622d17b24bcb83eb009a606806028898889e Mon Sep 17 00:00:00 2001 From: JMP Date: Tue, 26 Feb 2019 17:36:35 +0100 Subject: [PATCH 233/271] UUIDModel tests --- tests/test_models/test_uuid_model.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/test_models/test_uuid_model.py diff --git a/tests/test_models/test_uuid_model.py b/tests/test_models/test_uuid_model.py new file mode 100644 index 0000000..5559159 --- /dev/null +++ b/tests/test_models/test_uuid_model.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals + +from django.test import TestCase + +from tests.models import CustomUUIDModel, CustomNotPrimaryUUIDModel + + +class UUIDFieldTests(TestCase): + + def test_uuid_model_with_uuid_field_as_primary_key(self): + instance = CustomUUIDModel() + instance.save() + self.assertEqual(instance.id.__class__.__name__, 'UUID') + self.assertEqual(instance.id, instance.pk) + + def test_uuid_model_with_uuid_field_as_not_primary_key(self): + instance = CustomNotPrimaryUUIDModel() + instance.save() + self.assertEqual(instance.uuid.__class__.__name__, 'UUID') + self.assertNotEqual(instance.uuid, instance.pk) From 533501753f98b417b8569bc6ffab3f143fc4f64c Mon Sep 17 00:00:00 2001 From: JMP Date: Tue, 26 Feb 2019 17:36:59 +0100 Subject: [PATCH 234/271] UUID version custom exception --- model_utils/exceptions.py | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 model_utils/exceptions.py diff --git a/model_utils/exceptions.py b/model_utils/exceptions.py new file mode 100644 index 0000000..90566cb --- /dev/null +++ b/model_utils/exceptions.py @@ -0,0 +1,2 @@ +class UUIDVersionException(Exception): + pass From 430866abfaca568ed9486e4199d9aad3d07d5253 Mon Sep 17 00:00:00 2001 From: JMP Date: Tue, 26 Feb 2019 17:37:24 +0100 Subject: [PATCH 235/271] Update authors file --- AUTHORS.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 06a3e01..07e9967 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -50,4 +50,4 @@ | Jack Cushman | Zach Cheung | Daniel Andrlik - +| marfyl \ No newline at end of file From 015dd4831fbf887c7a26eb12802662ffd17d4949 Mon Sep 17 00:00:00 2001 From: JMP Date: Tue, 26 Feb 2019 17:50:32 +0100 Subject: [PATCH 236/271] Catch error with inherit class --- model_utils/fields.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/model_utils/fields.py b/model_utils/fields.py index 5bb630e..6c43ecc 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -1,14 +1,9 @@ from __future__ import unicode_literals import django -try: - import uuid # noqa - HAS_UUID = True -except ImportError: - HAS_UUID = False +import uuid from django.db import models from django.conf import settings -from django.core.exceptions import ImproperlyConfigured from django.utils.encoding import python_2_unicode_compatible from django.utils.timezone import now @@ -289,9 +284,6 @@ class UUIDField(models.UUIDField): UUIDVersionException UUID version 2 is not supported. """ - if not HAS_UUID: - raise ImproperlyConfigured("'uuid' module is required for UUIDField.") - kwargs.setdefault('primary_key', primary_key) kwargs.setdefault('editable', editable) From 2f142afd379a09ee69840e04e7a12fb279b13b65 Mon Sep 17 00:00:00 2001 From: JMP Date: Tue, 26 Feb 2019 17:55:02 +0100 Subject: [PATCH 237/271] Fix imports pep8 --- model_utils/models.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/model_utils/models.py b/model_utils/models.py index 0cfafe6..96c472b 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -1,12 +1,22 @@ from __future__ import unicode_literals -from model_utils.managers import QueryManager, SoftDeletableManager -from model_utils.fields import AutoCreatedField, AutoLastModifiedField, StatusField, MonitorField, UUIDField - import django from django.core.exceptions import ImproperlyConfigured from django.db import models from django.utils.translation import ugettext_lazy as _ + +from model_utils.fields import ( + AutoCreatedField, + AutoLastModifiedField, + StatusField, + MonitorField, + UUIDField, +) +from model_utils.managers import ( + QueryManager, + SoftDeletableManager, +) + if django.VERSION >= (1, 9, 0): from django.db.models.functions import Now now = Now() From e5955f780b57c5229a7cd26f219deb8bb5e061c7 Mon Sep 17 00:00:00 2001 From: jmmp Date: Tue, 26 Feb 2019 17:58:20 +0100 Subject: [PATCH 238/271] Update fields.py --- model_utils/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model_utils/fields.py b/model_utils/fields.py index 5bb630e..ff10e51 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -157,7 +157,7 @@ SPLIT_MARKER = getattr(settings, 'SPLIT_MARKER', '') SPLIT_DEFAULT_PARAGRAPHS = getattr(settings, 'SPLIT_DEFAULT_PARAGRAPHS', 2) -def _excerpt_field_name(name): return '_%s_excerpt' % name +_excerpt_field_name = lambda name: '_%s_excerpt' % name def get_excerpt(content): From 000c70de9eb6dae0a304a03cb3faf492548daca7 Mon Sep 17 00:00:00 2001 From: jmmp Date: Tue, 26 Feb 2019 18:04:13 +0100 Subject: [PATCH 239/271] Update fields.rst --- docs/fields.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fields.rst b/docs/fields.rst index 1f0d5c5..87c298e 100644 --- a/docs/fields.rst +++ b/docs/fields.rst @@ -159,7 +159,7 @@ be the excerpt. This number can be customized by setting the UUIDField ---------- -A ``UUIDField``subclass that provides an UUID field. You can +A ``UUIDField`` subclass that provides an UUID field. You can add this field to any model definition. With the param ``primary_key`` you can set if this field is the From a6fc51c0b5e57858c93770fb36d5444793b5c8ff Mon Sep 17 00:00:00 2001 From: JMP Date: Tue, 26 Feb 2019 18:10:42 +0100 Subject: [PATCH 240/271] Docs updated --- docs/fields.rst | 2 +- docs/models.rst | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/fields.rst b/docs/fields.rst index 1f0d5c5..87c298e 100644 --- a/docs/fields.rst +++ b/docs/fields.rst @@ -159,7 +159,7 @@ be the excerpt. This number can be customized by setting the UUIDField ---------- -A ``UUIDField``subclass that provides an UUID field. You can +A ``UUIDField`` subclass that provides an UUID field. You can add this field to any model definition. With the param ``primary_key`` you can set if this field is the diff --git a/docs/models.rst b/docs/models.rst index 4e04c29..48afa0d 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -64,7 +64,9 @@ This abstract base class provides ``id`` field on any model that inherits from i which will be the primary key. If you dont want to set ``id`` as primary key or change the field name, you can be override it -with our [UUIDField](https://github.com/jazzband/django-model-utils/blob/master/docs/fields.rst#uuidfield). +with our `UUIDField`_ + +(https://github.com/jazzband/django-model-utils/blob/master/docs/fields.rst#uuidfield). Also you can override the default uuid version. Versions 1,3,4 and 5 are now supported. @@ -76,3 +78,6 @@ Also you can override the default uuid version. Versions 1,3,4 and 5 are now sup class MyAppModel(UUIDModel): pass + + +.. _`UUIDField`: https://github.com/jazzband/django-model-utils/blob/master/docs/fields.rst#uuidfield \ No newline at end of file From 9b6f14bedd928eb670e0963c50f900f37e40481c Mon Sep 17 00:00:00 2001 From: jmmp Date: Tue, 26 Feb 2019 18:12:06 +0100 Subject: [PATCH 241/271] Update models.rst --- docs/models.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/models.rst b/docs/models.rst index 48afa0d..1c63c76 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -66,8 +66,6 @@ which will be the primary key. If you dont want to set ``id`` as primary key or change the field name, you can be override it with our `UUIDField`_ -(https://github.com/jazzband/django-model-utils/blob/master/docs/fields.rst#uuidfield). - Also you can override the default uuid version. Versions 1,3,4 and 5 are now supported. .. code-block:: python @@ -80,4 +78,4 @@ 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 \ No newline at end of file +.. _`UUIDField`: https://github.com/jazzband/django-model-utils/blob/master/docs/fields.rst#uuidfield From 8d9a00b89b2e9b5125c4e753aaa92f1e0a953d6a Mon Sep 17 00:00:00 2001 From: JMP Date: Tue, 26 Feb 2019 18:16:01 +0100 Subject: [PATCH 242/271] Fix pep8 error --- tests/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/models.py b/tests/models.py index 72e48a4..ee1208e 100644 --- a/tests/models.py +++ b/tests/models.py @@ -165,8 +165,8 @@ class Post(models.Model): objects = models.Manager() public = QueryManager(published=True) - public_confirmed = QueryManager(models.Q(published=True) - & models.Q(confirmed=True)) + public_confirmed = QueryManager( + models.Q(published=True) & models.Q(confirmed=True)) public_reversed = QueryManager(published=True).order_by("-order") class Meta: From da3c59a6df56b5974823394ce0d4c7fd75efad63 Mon Sep 17 00:00:00 2001 From: JMP Date: Tue, 26 Feb 2019 18:38:39 +0100 Subject: [PATCH 243/271] Fix SyntaxError for python 2.7 --- model_utils/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/model_utils/fields.py b/model_utils/fields.py index e0ac7ba..6ad2b66 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -152,7 +152,7 @@ SPLIT_MARKER = getattr(settings, 'SPLIT_MARKER', '') SPLIT_DEFAULT_PARAGRAPHS = getattr(settings, 'SPLIT_DEFAULT_PARAGRAPHS', 2) -_excerpt_field_name = lambda name: '_%s_excerpt' % name +def _excerpt_field_name(name): return '_%s_excerpt' % name def get_excerpt(content): @@ -264,7 +264,7 @@ class SplitField(models.TextField): class UUIDField(models.UUIDField): """ - A field for storing universally unique identifiers. Uses Python’s UUID class. + A field for storing universally unique identifiers. Use Python UUID class. """ def __init__(self, primary_key=True, version=4, editable=False, *args, **kwargs): From 5c49f2c7d00a24728b825652f2a1e5382a9985d7 Mon Sep 17 00:00:00 2001 From: JMP Date: Tue, 26 Feb 2019 18:41:23 +0100 Subject: [PATCH 244/271] Update changes.rst for UUIDModel and UUIDField --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index 629c4dc..9f2e6ff 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,7 @@ master (unreleased) - Fix patched `save` in FieldTracker - Upgrades test requirements (pytest, pytest-django, pytest-cov) and skips tox test with Python 3.5 and Django (trunk) +- Add UUIDModel and UUIDField support. 3.1.2 (2018.05.09) ------------------ From 2a47aa093de48e135a83733fef3ea6cbeac5ff0e Mon Sep 17 00:00:00 2001 From: JMP Date: Tue, 26 Feb 2019 19:01:39 +0100 Subject: [PATCH 245/271] Pep8 review --- model_utils/fields.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/model_utils/fields.py b/model_utils/fields.py index 6ad2b66..5816d7a 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -292,13 +292,15 @@ class UUIDField(models.UUIDField): elif version == 1: default = uuid.uuid1 elif version == 2: - raise UUIDVersionException("UUID version 2 is not supported.") + raise UUIDVersionException( + 'UUID version 2 is not supported.') elif version == 3: default = uuid.uuid3 elif version == 5: default = uuid.uuid5 else: - raise UUIDVersionException("UUID version %s is not valid." % version) + raise UUIDVersionException( + 'UUID version is not valid.') kwargs.setdefault('default', default) super(UUIDField, self).__init__(*args, **kwargs) From ce3a0e59f4f552c6e2ae24e32587c22d5d5c9f07 Mon Sep 17 00:00:00 2001 From: JMP Date: Tue, 26 Feb 2019 19:17:06 +0100 Subject: [PATCH 246/271] Use django exception instead custom one --- model_utils/exceptions.py | 2 -- model_utils/fields.py | 12 ++++++------ 2 files changed, 6 insertions(+), 8 deletions(-) delete mode 100644 model_utils/exceptions.py diff --git a/model_utils/exceptions.py b/model_utils/exceptions.py deleted file mode 100644 index 90566cb..0000000 --- a/model_utils/exceptions.py +++ /dev/null @@ -1,2 +0,0 @@ -class UUIDVersionException(Exception): - pass diff --git a/model_utils/fields.py b/model_utils/fields.py index 5816d7a..952b726 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -4,11 +4,10 @@ import django import uuid from django.db import models from django.conf import settings +from django.core.exceptions import ValidationError from django.utils.encoding import python_2_unicode_compatible from django.utils.timezone import now -from model_utils.exceptions import UUIDVersionException - DEFAULT_CHOICES_NAME = 'STATUS' @@ -152,7 +151,8 @@ SPLIT_MARKER = getattr(settings, 'SPLIT_MARKER', '') SPLIT_DEFAULT_PARAGRAPHS = getattr(settings, 'SPLIT_DEFAULT_PARAGRAPHS', 2) -def _excerpt_field_name(name): return '_%s_excerpt' % name +def _excerpt_field_name(name): + return '_%s_excerpt' % name def get_excerpt(content): @@ -281,7 +281,7 @@ class UUIDField(models.UUIDField): Raises ------ - UUIDVersionException + ValidationError UUID version 2 is not supported. """ kwargs.setdefault('primary_key', primary_key) @@ -292,14 +292,14 @@ class UUIDField(models.UUIDField): elif version == 1: default = uuid.uuid1 elif version == 2: - raise UUIDVersionException( + raise ValidationError( 'UUID version 2 is not supported.') elif version == 3: default = uuid.uuid3 elif version == 5: default = uuid.uuid5 else: - raise UUIDVersionException( + raise ValidationError( 'UUID version is not valid.') kwargs.setdefault('default', default) From 89fd5fff82809266b119b14067aa9856c7e9ba7a Mon Sep 17 00:00:00 2001 From: JMP Date: Tue, 26 Feb 2019 19:19:32 +0100 Subject: [PATCH 247/271] Catch exception in test --- tests/test_fields/test_uuid_field.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_fields/test_uuid_field.py b/tests/test_fields/test_uuid_field.py index 74faa4f..85341f4 100644 --- a/tests/test_fields/test_uuid_field.py +++ b/tests/test_fields/test_uuid_field.py @@ -2,10 +2,10 @@ from __future__ import unicode_literals import uuid +from django.core.exceptions import ValidationError from django.test import TestCase from model_utils.fields import UUIDField -from model_utils.exceptions import UUIDVersionException class UUIDFieldTests(TestCase): @@ -19,7 +19,7 @@ class UUIDFieldTests(TestCase): self.assertEqual(instance.default, uuid.uuid1) def test_uuid_version_2_error(self): - self.assertRaises(UUIDVersionException, UUIDField, 'version', 2) + self.assertRaises(ValidationError, UUIDField, 'version', 2) def test_uuid_version_3(self): instance = UUIDField(version=3) From 5ffcfe831c609ecd4c2f781e789fbe3c05e913b7 Mon Sep 17 00:00:00 2001 From: jmmp Date: Wed, 27 Feb 2019 02:29:47 +0100 Subject: [PATCH 248/271] Update fields.py --- model_utils/fields.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/model_utils/fields.py b/model_utils/fields.py index 952b726..701147b 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -284,23 +284,25 @@ class UUIDField(models.UUIDField): ValidationError UUID version 2 is not supported. """ - kwargs.setdefault('primary_key', primary_key) - kwargs.setdefault('editable', editable) - - if version == 4: - default = uuid.uuid4 - elif version == 1: - default = uuid.uuid1 - elif version == 2: + + if version == 2: raise ValidationError( 'UUID version 2 is not supported.') - elif version == 3: - default = uuid.uuid3 - elif version == 5: - default = uuid.uuid5 - else: + + if version < 1 or version > 5: raise ValidationError( 'UUID version is not valid.') - + + if version == 1: + default = uuid.uuid1 + elif version == 3: + default = uuid.uuid3 + elif version == 4: + default = uuid.uuid4 + elif version == 5: + default = uuid.uuid5 + + kwargs.setdefault('primary_key', primary_key) + kwargs.setdefault('editable', editable) kwargs.setdefault('default', default) super(UUIDField, self).__init__(*args, **kwargs) From 434bc6d45c59c5794ab9ecf22ade8ec27274db51 Mon Sep 17 00:00:00 2001 From: jmmp Date: Wed, 27 Feb 2019 14:33:48 +0100 Subject: [PATCH 249/271] unneeded import removed --- docs/models.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/models.rst b/docs/models.rst index 1c63c76..255826c 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -71,7 +71,6 @@ Also you can override the default uuid version. Versions 1,3,4 and 5 are now sup .. code-block:: python from model_utils.models import UUIDModel - from model_utils import Choices class MyAppModel(UUIDModel): pass From 581522c46a3497aed74e275c396dcfbb64e0c55a Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Mon, 11 Mar 2019 14:51:59 +0700 Subject: [PATCH 250/271] MANIFEST.in: Add docs and tests (#362) Also include all language translations in sdist. Fixes https://github.com/jazzband/django-model-utils/issues/361 and closes https://github.com/jazzband/django-model-utils/issues/356 --- MANIFEST.in | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 51df88e..9063cef 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,4 +3,8 @@ include CHANGES.rst include LICENSE.txt include MANIFEST.in include README.rst -include model_utils/locale/de/LC_MESSAGES/django.po +include requirements*.txt +include Makefile tox.ini +recursive-include model_utils/locale *.po *.mo +graft docs +recursive-include tests *.py From 1b9b5ac2c180ece8d418ddbf277e9d944979003a Mon Sep 17 00:00:00 2001 From: Skia Date: Tue, 12 Mar 2019 18:29:02 +0100 Subject: [PATCH 251/271] managers: honor OneToOneField.parent_link=False (#363) --- CHANGES.rst | 1 + model_utils/managers.py | 1 + tests/models.py | 10 ++++++++++ tests/test_managers/test_inheritance_manager.py | 7 +++++-- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 629c4dc..41ef7de 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,7 @@ CHANGES master (unreleased) ------------------- +- Honor `OneToOneField.parent_link=False`. - Fix handling of deferred attributes on Django 1.10+, fixes GH-278 - Fix `FieldTracker.has_changed()` and `FieldTracker.previous()` to return correct responses for deferred fields. diff --git a/model_utils/managers.py b/model_utils/managers.py index 15b5c7c..7161a33 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -158,6 +158,7 @@ class InheritanceQuerySetMixin(object): if isinstance(rel.field, OneToOneField) and issubclass(rel.field.model, model) and model is not rel.field.model + and rel.parent_link ] subclasses = [] diff --git a/tests/models.py b/tests/models.py index 888aba5..4e33a14 100644 --- a/tests/models.py +++ b/tests/models.py @@ -73,6 +73,16 @@ class InheritanceManagerTestChild3(InheritanceManagerTestParent): InheritanceManagerTestParent, related_name='manual_onetoone', parent_link=True, on_delete=models.CASCADE) +class InheritanceManagerTestChild4(InheritanceManagerTestParent): + other_onetoone = models.OneToOneField( + InheritanceManagerTestParent, related_name='non_inheritance_relation', + parent_link=False, on_delete=models.CASCADE) + # The following is needed because of that Django bug: + # https://code.djangoproject.com/ticket/29998 + parent_ptr = models.OneToOneField( + InheritanceManagerTestParent, related_name='child4_onetoone', + parent_link=True, on_delete=models.CASCADE) + class TimeStamp(TimeStampedModel): pass diff --git a/tests/test_managers/test_inheritance_manager.py b/tests/test_managers/test_inheritance_manager.py index d2b8b4f..a72aa61 100644 --- a/tests/test_managers/test_inheritance_manager.py +++ b/tests/test_managers/test_inheritance_manager.py @@ -10,7 +10,8 @@ from tests.models import ( InheritanceManagerTestRelated, InheritanceManagerTestGrandChild1, InheritanceManagerTestGrandChild1_2, InheritanceManagerTestParent, InheritanceManagerTestChild1, - InheritanceManagerTestChild2, TimeFrame, InheritanceManagerTestChild3 + InheritanceManagerTestChild2, TimeFrame, InheritanceManagerTestChild3, + InheritanceManagerTestChild4, ) @@ -141,6 +142,7 @@ class InheritanceManagerTests(TestCase): 'inheritancemanagertestchild1', 'inheritancemanagertestchild2', 'manual_onetoone', # this was set via parent_link & related_name + 'child4_onetoone', ] self.assertEqual(set(results.subclasses), set(expected_related_names)) @@ -258,7 +260,7 @@ class InheritanceManagerUsingModelsTests(TestCase): objs = InheritanceManagerTestParent.objects.select_subclasses().order_by('pk') objsmodels = InheritanceManagerTestParent.objects.select_subclasses( InheritanceManagerTestChild1, InheritanceManagerTestChild2, - InheritanceManagerTestChild3, + InheritanceManagerTestChild3, InheritanceManagerTestChild4, InheritanceManagerTestGrandChild1, InheritanceManagerTestGrandChild1_2).order_by('pk') self.assertEqual(set(objs.subclasses), set(objsmodels.subclasses)) @@ -280,6 +282,7 @@ class InheritanceManagerUsingModelsTests(TestCase): models = (InheritanceManagerTestChild1, InheritanceManagerTestChild2, InheritanceManagerTestChild3, + InheritanceManagerTestChild4, InheritanceManagerTestGrandChild1, InheritanceManagerTestGrandChild1_2) From c4a252d1fb38c8fa7c645bc9292712e2fdf4d7c1 Mon Sep 17 00:00:00 2001 From: Remy Suen Date: Wed, 20 Mar 2019 05:07:00 -0400 Subject: [PATCH 252/271] Explain usage of timeframed model manager in the documentation (#365) * Provide a sample for using the timeframed manager Signed-off-by: Remy Suen * Update CHANGES.rst file Signed-off-by: Remy Suen * Update AUTHORS.rst file Signed-off-by: Remy Suen --- AUTHORS.rst | 1 + CHANGES.rst | 1 + docs/models.rst | 37 ++++++++++++++++++++++++++++++++++--- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 06a3e01..80d8f44 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -33,6 +33,7 @@ | Patryk Zawadzki | Paul McLanahan | Philipp Steinhardt +| Remy Suen | Rinat Shigapov | Rodney Folz | Romain Garrigues diff --git a/CHANGES.rst b/CHANGES.rst index 41ef7de..12d525f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,7 @@ CHANGES master (unreleased) ------------------- +- Update documentation to explain usage of `timeframed` model manager, fixes GH-118 - Honor `OneToOneField.parent_link=False`. - Fix handling of deferred attributes on Django 1.10+, fixes GH-278 - Fix `FieldTracker.has_changed()` and `FieldTracker.previous()` to return diff --git a/docs/models.rst b/docs/models.rst index 51bde8f..5ddbd08 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -5,10 +5,41 @@ TimeFramedModel --------------- An abstract base class for any model that expresses a time-range. Adds -``start`` and ``end`` nullable DateTimeFields, and a ``timeframed`` -manager that returns only objects for whom the current date-time lies -within their time range. +``start`` and ``end`` nullable DateTimeFields, and provides a new +``timeframed`` manager on the subclass whose queryset pre-filters results +to only include those which have a ``start`` which is not in the future, +and an ``end`` which is not in the past. If either ``start`` or ``end`` is +``null``, the manager will include it. +.. code-block:: python + + from model_utils.models import TimeFramedModel + from datetime import datetime, timedelta + class Post(TimeFramedModel): + pass + + p = Post() + p.start = datetime.utcnow() - timedelta(days=1) + p.end = datetime.utcnow() + timedelta(days=7) + p.save() + + # this query will return the above Post instance: + Post.timeframed.all() + + p.start = None + p.end = None + p.save() + + # this query will also return the above Post instance, because + # the `start` and/or `end` are NULL. + Post.timeframed.all() + + p.start = datetime.utcnow() + timedelta(days=7) + p.save() + + # this query will NOT return our Post instance, because + # the start date is in the future. + Post.timeframed.all() TimeStampedModel ---------------- From fc523d543392b9ef7d5b8b6c8ec962b151552e42 Mon Sep 17 00:00:00 2001 From: JMP Date: Wed, 20 Mar 2019 21:51:31 +0100 Subject: [PATCH 253/271] Add tdd to increase coverage --- tests/test_fields/test_uuid_field.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_fields/test_uuid_field.py b/tests/test_fields/test_uuid_field.py index 85341f4..3a6c739 100644 --- a/tests/test_fields/test_uuid_field.py +++ b/tests/test_fields/test_uuid_field.py @@ -32,3 +32,9 @@ class UUIDFieldTests(TestCase): def test_uuid_version_5(self): instance = UUIDField(version=5) self.assertEqual(instance.default, uuid.uuid5) + + def test_uuid_version_bellow_min(self): + self.assertRaises(ValidationError, UUIDField, 'version', 0) + + def test_uuid_version_above_max(self): + self.assertRaises(ValidationError, UUIDField, 'version', 6) From d557c4253312774a7c2f14bcd02675e9ac2ea05f Mon Sep 17 00:00:00 2001 From: Nick Sandford Date: Fri, 29 Mar 2019 17:46:40 +1100 Subject: [PATCH 254/271] Catch deferred attribute exception (#367) --- CHANGES.rst | 3 ++- model_utils/tracker.py | 5 ++++- tests/models.py | 15 +++++++++++++++ tests/test_fields/test_field_tracker.py | 7 ++++++- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 12d525f..ff9abf0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,7 @@ CHANGES master (unreleased) ------------------- +- Catch `AttributeError` for deferred abstract fields, fixes GH-331. - Update documentation to explain usage of `timeframed` model manager, fixes GH-118 - Honor `OneToOneField.parent_link=False`. - Fix handling of deferred attributes on Django 1.10+, fixes GH-278 @@ -14,7 +15,7 @@ master (unreleased) - Support `reversed` for all kinds of `Choices` objects, fixes GH-309 - Fix Model instance non picklable GH-330 - Fix patched `save` in FieldTracker -- Upgrades test requirements (pytest, pytest-django, pytest-cov) and +- Upgrades test requirements (pytest, pytest-django, pytest-cov) and skips tox test with Python 3.5 and Django (trunk) 3.1.2 (2018.05.09) diff --git a/model_utils/tracker.py b/model_utils/tracker.py index 837e1ce..6f50d10 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -40,7 +40,10 @@ class DescriptorWrapper(object): if instance is None: return self was_deferred = self.field_name in instance.get_deferred_fields() - value = self.descriptor.__get__(instance, owner) + try: + value = self.descriptor.__get__(instance, owner) + except AttributeError: + value = self.descriptor if was_deferred: tracker_instance = getattr(instance, self.tracker_attname) tracker_instance.saved_data[self.field_name] = deepcopy(value) diff --git a/tests/models.py b/tests/models.py index 4e33a14..ef38626 100644 --- a/tests/models.py +++ b/tests/models.py @@ -224,6 +224,13 @@ class FeaturedManager(models.Manager): return ByAuthorQuerySet(self.model, **kwargs).filter(feature=True) +class AbstractTracked(models.Model): + number = 1 + + class Meta: + abstract = True + + class Tracked(models.Model): name = models.CharField(max_length=20) number = models.IntegerField() @@ -240,6 +247,14 @@ class TrackedFK(models.Model): custom_tracker_without_id = FieldTracker(fields=['fk']) +class TrackedAbstract(AbstractTracked): + name = models.CharField(max_length=20) + number = models.IntegerField() + mutable = MutableField(default=None) + + tracker = FieldTracker() + + class TrackedNotDefault(models.Model): name = models.CharField(max_length=20) number = models.IntegerField() diff --git a/tests/test_fields/test_field_tracker.py b/tests/test_fields/test_field_tracker.py index 5b5d5c2..83cde07 100644 --- a/tests/test_fields/test_field_tracker.py +++ b/tests/test_fields/test_field_tracker.py @@ -8,7 +8,7 @@ from model_utils import FieldTracker from model_utils.tracker import DescriptorWrapper from tests.models import ( Tracked, TrackedFK, InheritedTrackedFK, TrackedNotDefault, TrackedNonFieldAttr, TrackedMultiple, - InheritedTracked, TrackedFileField, + InheritedTracked, TrackedFileField, TrackedAbstract, ModelTracked, ModelTrackedFK, ModelTrackedNotDefault, ModelTrackedMultiple, InheritedModelTracked, ) @@ -785,3 +785,8 @@ class InheritedModelTrackerTests(ModelTrackerTests): self.name2 = 'test' self.assertEqual(self.tracker.previous('name2'), None) self.assertTrue(self.tracker.has_changed('name2')) + + +class AbstractModelTrackerTests(FieldTrackerTestCase): + + tracked_class = TrackedAbstract From f2e97e41418788c81ee8e5ab1cec37ecff65a003 Mon Sep 17 00:00:00 2001 From: Jack Cushman Date: Thu, 25 Apr 2019 17:50:08 -0400 Subject: [PATCH 255/271] Move patch_save to finalize_class so it works on models with save() methods --- model_utils/tracker.py | 2 +- tests/models.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/model_utils/tracker.py b/model_utils/tracker.py index 6f50d10..5c7c7d8 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -210,7 +210,6 @@ class FieldTracker(object): def contribute_to_class(self, cls, name): self.name = name self.attname = '_%s' % name - self.patch_save(cls) models.signals.class_prepared.connect(self.finalize_class, sender=cls) def finalize_class(self, sender, **kwargs): @@ -227,6 +226,7 @@ class FieldTracker(object): models.signals.post_init.connect(self.initialize_tracker) self.model_class = sender setattr(sender, self.name, self) + self.patch_save(sender) def initialize_tracker(self, sender, instance, **kwargs): if not isinstance(instance, self.model_class): diff --git a/tests/models.py b/tests/models.py index ef38626..bab9d4b 100644 --- a/tests/models.py +++ b/tests/models.py @@ -238,6 +238,10 @@ class Tracked(models.Model): tracker = FieldTracker() + def save(self, *args, **kwargs): + """ No-op save() to ensure that FieldTracker.patch_save() works. """ + super(Tracked, self).save(*args, **kwargs) + class TrackedFK(models.Model): fk = models.ForeignKey('Tracked', on_delete=models.CASCADE) From 3c6bd237afdb6ce61f0d868f768eddeb771f6a33 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Thu, 16 May 2019 05:22:00 +0600 Subject: [PATCH 256/271] drop py34 add py37 --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a7d4622..80385ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,7 @@ language: python cache: pip python: - 2.7 -- 3.4 -- 3.5 +- 3.7 - 3.6 install: pip install tox-travis codecov # positional args ({posargs}) to pass into tox.ini From 98b7f18047ec2cca3e43a946bfabf78341c632d1 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Thu, 16 May 2019 05:24:49 +0600 Subject: [PATCH 257/271] django 1.11 n 2.1+ only --- tox.ini | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tox.ini b/tox.ini index 4cdf852..ca2f2e3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,17 +1,14 @@ [tox] envlist = - py27-django{19,110,111} - py34-django{19,110,111,200} - py35-django{19,110,111,200,201} - py36-django{111,200,201,trunk} + py27-django{111} + py37-django{202,201} + py36-django{111,202,201,trunk} flake8 [testenv] deps = - django19: Django>=1.9,<1.10 - django110: Django>=1.10,<1.11 django111: Django>=1.11,<1.12 - django200: Django>=2.0,<2.1 + django202: Django>=2.2,<3.0 django201: Django>=2.1,<2.2 djangotrunk: https://github.com/django/django/archive/master.tar.gz freezegun == 0.3.8 From a2f46f74d733cc98c85d9dbc064f29b2658dd6fe Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Thu, 16 May 2019 05:25:55 +0600 Subject: [PATCH 258/271] update supported versions --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 6d74e7a..490e170 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ django-model-utils Django model mixins and utilities. -``django-model-utils`` supports `Django`_ 1.8 to 2.1. +``django-model-utils`` supports `Django`_ 1.11 and 2.1+. .. _Django: http://www.djangoproject.com/ From 5d73431e0d81da5a1271d4b0c04f41369766b30b Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Thu, 16 May 2019 05:27:36 +0600 Subject: [PATCH 259/271] xenial --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 80385ee..e2fd9a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ -sudo: false +sudo: True +dist: xenial language: python cache: pip python: From e04ef1d9ba55d63da66945ae9cc97590d52a8fdb Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Thu, 16 May 2019 05:30:03 +0600 Subject: [PATCH 260/271] adjust setup --- setup.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 067a64b..5b61beb 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ setup( maintainer='JazzBand', url='https://github.com/jazzband/django-model-utils/', packages=find_packages(exclude=['tests*']), - install_requires=['Django>=1.8'], + install_requires=['Django>=1.11'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', @@ -43,19 +43,15 @@ setup( 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.6', 'Framework :: Django', - 'Framework :: Django :: 1.8', - 'Framework :: Django :: 1.9', - 'Framework :: Django :: 1.10', + 'Framework :: Django :: 2.1', 'Framework :: Django :: 1.11', - 'Framework :: Django :: 2.0', + 'Framework :: Django :: 2.2', ], zip_safe=False, - tests_require=['Django>=1.8'], + tests_require=['Django>=1.1.11'], package_data={ 'model_utils': [ 'locale/*/LC_MESSAGES/django.po', 'locale/*/LC_MESSAGES/django.mo' From 77bbdb12fcaea3a25a1c20e7cb4a195857bca944 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Thu, 16 May 2019 05:42:33 +0600 Subject: [PATCH 261/271] updated dependencies --- requirements-test.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index f8c6741..f190999 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ -pytest==4.3.0 +pytest==4.5.0 pytest-django==3.4.7 psycopg2==2.7.6.1 -pytest-cov==2.6.1 \ No newline at end of file +pytest-cov==2.7.1 From b7d953d8bb605c9d57181ac44292e434bed6f1e9 Mon Sep 17 00:00:00 2001 From: Emin Bugra Saral Date: Mon, 10 Jul 2017 13:26:07 +0200 Subject: [PATCH 262/271] Add requirement for running tests --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 0396424..f7c07a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ tox sphinx twine +freezegun From 5d6f8f4e9f0c93b4cb2da2b42efeb40b299ec291 Mon Sep 17 00:00:00 2001 From: Emin Bugra Saral Date: Mon, 10 Jul 2017 14:35:39 +0200 Subject: [PATCH 263/271] Disable signals on save feature --- docs/models.rst | 19 +++++- model_utils/models.py | 60 ++++++++++++++++++- tests/models.py | 6 ++ tests/signals.py | 5 ++ .../test_savesignalhandling_model.py | 45 ++++++++++++++ 5 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 tests/signals.py create mode 100644 tests/test_models/test_savesignalhandling_model.py diff --git a/docs/models.rst b/docs/models.rst index a2aac4d..31707f4 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -107,5 +107,22 @@ Also you can override the default uuid version. Versions 1,3,4 and 5 are now sup pass - .. _`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/model_utils/models.py b/model_utils/models.py index 96c472b..43c34dd 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -2,7 +2,8 @@ from __future__ import unicode_literals import django from django.core.exceptions import ImproperlyConfigured -from django.db import models +from django.db import models, transaction, router +from django.db.models.signals import post_save, pre_save from django.utils.translation import ugettext_lazy as _ from model_utils.fields import ( @@ -159,3 +160,60 @@ 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(SaveSignalHandlingModel, self).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 not 'pre_save' 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 not 'post_save' 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/tests/models.py b/tests/models.py index 6a1f822..40e3ff6 100644 --- a/tests/models.py +++ b/tests/models.py @@ -4,6 +4,7 @@ import django from django.db import models from django.db.models.query_utils import DeferredAttribute from django.db.models import Manager +from django.dispatch import receiver from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ @@ -25,6 +26,7 @@ from model_utils.models import ( TimeFramedModel, TimeStampedModel, UUIDModel, + SaveSignalHandlingModel, ) from tests.fields import MutableField from tests.managers import CustomSoftDeleteManager @@ -437,3 +439,7 @@ class CustomUUIDModel(UUIDModel): class CustomNotPrimaryUUIDModel(models.Model): uuid = UUIDField(primary_key=False) + + +class SaveSignalHandlingTestModel(SaveSignalHandlingModel): + name = models.CharField(max_length=20) diff --git a/tests/signals.py b/tests/signals.py new file mode 100644 index 0000000..f75efbe --- /dev/null +++ b/tests/signals.py @@ -0,0 +1,5 @@ +def pre_save_test(instance, *args, **kwargs): + instance.pre_save_runned = True + +def post_save_test(instance, created, *args, **kwargs): + instance.post_save_runned = True diff --git a/tests/test_models/test_savesignalhandling_model.py b/tests/test_models/test_savesignalhandling_model.py new file mode 100644 index 0000000..e281f78 --- /dev/null +++ b/tests/test_models/test_savesignalhandling_model.py @@ -0,0 +1,45 @@ +from __future__ import unicode_literals + +from django.test import TestCase + +from tests.models import SaveSignalHandlingTestModel +from tests.signals import pre_save_test, post_save_test +from django.db.models.signals import pre_save, post_save + + +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')) From f5d4e5ce2b27fa99b0494735490689bcfe5bce5d Mon Sep 17 00:00:00 2001 From: Emin Bugra Saral Date: Mon, 20 May 2019 19:45:51 +0200 Subject: [PATCH 264/271] Update changes and authors --- AUTHORS.rst | 1 + CHANGES.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index bd4eef6..1c56aa2 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -11,6 +11,7 @@ | Dmytro Kyrychuk | Donald Stufft | Douglas Meehan +| Emin Bugra Saral | Facundo Gaich | Felipe Prenholato | Filipe Ximenes diff --git a/CHANGES.rst b/CHANGES.rst index dbb6a1a..9c6257e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -46,6 +46,7 @@ master (unreleased) * Drop support for Django 1.4, 1.5, 1.6, 1.7. * Exclude tests from the distribution, fixes GH-258. * Add support for Django 1.11 GH-269 +* Add a new model to disable pre_save/post_save signals 2.6.1 (2017.01.11) From 08e84eff69dbf668b882ba57b3d73539fbf46b6f Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Fri, 21 Jun 2019 11:18:00 -0400 Subject: [PATCH 265/271] Release 3.2.0. --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9c6257e..24a97ff 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,7 @@ CHANGES ======= -master (unreleased) +3.2.0 (2019.06.21) ------------------- - Catch `AttributeError` for deferred abstract fields, fixes GH-331. - Update documentation to explain usage of `timeframed` model manager, fixes GH-118 From 383740e8ab54008d909de1b7166b9d8840883392 Mon Sep 17 00:00:00 2001 From: asday Date: Mon, 19 Aug 2019 22:29:19 +0100 Subject: [PATCH 266/271] Added `Choices().subset()`. --- model_utils/choices.py | 14 ++++++++++++++ tests/test_choices.py | 27 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/model_utils/choices.py b/model_utils/choices.py index 6339503..31d5aa1 100644 --- a/model_utils/choices.py +++ b/model_utils/choices.py @@ -142,3 +142,17 @@ class Choices(object): def __deepcopy__(self, memo): return self.__class__(*copy.deepcopy(self._triples, memo)) + + def subset(self, *new_identifiers): + identifiers = set(self._identifier_map.keys()) + + if not identifiers.issuperset(new_identifiers): + raise ValueError( + 'The following identifiers are not present: %s' % + identifiers.symmetric_difference(new_identifiers), + ) + + return self.__class__(*[ + choice for choice in self._triples + if choice[1] in new_identifiers + ]) diff --git a/tests/test_choices.py b/tests/test_choices.py index 986670c..cb5bec9 100644 --- a/tests/test_choices.py +++ b/tests/test_choices.py @@ -279,3 +279,30 @@ class IdentifierChoicesTests(ChoicesTests): ('group b', [(3, 'three')]), ], ) + + +class SubsetChoicesTest(TestCase): + + def setUp(self): + self.choices = Choices( + (0, 'a', 'A'), + (1, 'b', 'B'), + ) + + def test_nonexistent_identifiers_raise(self): + with self.assertRaises(ValueError): + self.choices.subset('a', 'c') + + def test_solo_nonexistent_identifiers_raise(self): + with self.assertRaises(ValueError): + self.choices.subset('c') + + def test_empty_subset_passes(self): + subset = self.choices.subset() + + self.assertEqual(subset, Choices()) + + def test_subset_returns_correct_subset(self): + subset = self.choices.subset('a') + + self.assertEqual(subset, Choices((0, 'a', 'A'))) From b7483fa538c2180852f42d48128d8b1dd6d5749c Mon Sep 17 00:00:00 2001 From: asday Date: Mon, 19 Aug 2019 22:37:01 +0100 Subject: [PATCH 267/271] Removed an unused import. --- tests/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/models.py b/tests/models.py index 40e3ff6..a54bdca 100644 --- a/tests/models.py +++ b/tests/models.py @@ -4,7 +4,6 @@ import django from django.db import models from django.db.models.query_utils import DeferredAttribute from django.db.models import Manager -from django.dispatch import receiver from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ From 51a720386c483ed8c29aaa363b4c34cf80a543c2 Mon Sep 17 00:00:00 2001 From: asday Date: Mon, 19 Aug 2019 22:37:16 +0100 Subject: [PATCH 268/271] Moved a binary operator to the start of a line. --- model_utils/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/model_utils/models.py b/model_utils/models.py index 43c34dd..26cec0b 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -107,8 +107,8 @@ def add_timeframed_query_manager(sender, **kwargs): % sender.__name__ ) sender.add_to_class('timeframed', QueryManager( - (models.Q(start__lte=now) | models.Q(start__isnull=True)) & - (models.Q(end__gte=now) | models.Q(end__isnull=True)) + (models.Q(start__lte=now) | models.Q(start__isnull=True)) + & (models.Q(end__gte=now) | models.Q(end__isnull=True)) )) From d9aa34e498294c4d90a6143da91bf8ee7f477349 Mon Sep 17 00:00:00 2001 From: asday Date: Mon, 19 Aug 2019 22:38:05 +0100 Subject: [PATCH 269/271] Fixed spacing. Extraneous whitespace, too many blank lines, and too few blank lines. --- model_utils/fields.py | 10 +++++----- tests/models.py | 1 + tests/signals.py | 1 + tests/test_models/test_savesignalhandling_model.py | 1 - 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/model_utils/fields.py b/model_utils/fields.py index 701147b..989c166 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -284,15 +284,15 @@ class UUIDField(models.UUIDField): ValidationError UUID version 2 is not supported. """ - + if version == 2: raise ValidationError( 'UUID version 2 is not supported.') - + if version < 1 or version > 5: raise ValidationError( 'UUID version is not valid.') - + if version == 1: default = uuid.uuid1 elif version == 3: @@ -300,8 +300,8 @@ class UUIDField(models.UUIDField): elif version == 4: default = uuid.uuid4 elif version == 5: - default = uuid.uuid5 - + default = uuid.uuid5 + kwargs.setdefault('primary_key', primary_key) kwargs.setdefault('editable', editable) kwargs.setdefault('default', default) diff --git a/tests/models.py b/tests/models.py index a54bdca..9c5e374 100644 --- a/tests/models.py +++ b/tests/models.py @@ -80,6 +80,7 @@ class InheritanceManagerTestChild3(InheritanceManagerTestParent): InheritanceManagerTestParent, related_name='manual_onetoone', parent_link=True, on_delete=models.CASCADE) + class InheritanceManagerTestChild4(InheritanceManagerTestParent): other_onetoone = models.OneToOneField( InheritanceManagerTestParent, related_name='non_inheritance_relation', diff --git a/tests/signals.py b/tests/signals.py index f75efbe..6b86ca2 100644 --- a/tests/signals.py +++ b/tests/signals.py @@ -1,5 +1,6 @@ def pre_save_test(instance, *args, **kwargs): instance.pre_save_runned = True + def post_save_test(instance, created, *args, **kwargs): instance.post_save_runned = True diff --git a/tests/test_models/test_savesignalhandling_model.py b/tests/test_models/test_savesignalhandling_model.py index e281f78..6af0820 100644 --- a/tests/test_models/test_savesignalhandling_model.py +++ b/tests/test_models/test_savesignalhandling_model.py @@ -26,7 +26,6 @@ class SaveSignalHandlingModelTests(TestCase): 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) From 1142254f6200fe826e2b95e1ff1bf29ece8acd08 Mon Sep 17 00:00:00 2001 From: asday Date: Mon, 19 Aug 2019 22:38:30 +0100 Subject: [PATCH 270/271] `not x in y` => `x not in y`. --- model_utils/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/model_utils/models.py b/model_utils/models.py index 26cec0b..6083015 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -196,7 +196,7 @@ class SaveSignalHandlingModel(models.Model): if cls._meta.proxy: cls = cls._meta.concrete_model meta = cls._meta - if not meta.auto_created and not 'pre_save' in self.signals_to_disable: + 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, @@ -209,7 +209,7 @@ class SaveSignalHandlingModel(models.Model): self._state.db = using self._state.adding = False - if not meta.auto_created and not 'post_save' in self.signals_to_disable: + 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, From 488be3ff0176a99a4b42ef8cf83750a8e7b35319 Mon Sep 17 00:00:00 2001 From: asday Date: Mon, 19 Aug 2019 23:02:43 +0100 Subject: [PATCH 271/271] Updated documentation and bumped minor version. --- AUTHORS.rst | 1 + CHANGES.rst | 5 ++++- docs/utilities.rst | 21 +++++++++++++++++++++ model_utils/__init__.py | 2 +- 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 1c56aa2..609d55e 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,4 +1,5 @@ | ad-m +| Adam Barnes | Alejandro Varas | Alex Orange | Alexey Evseev diff --git a/CHANGES.rst b/CHANGES.rst index 24a97ff..abd1a8e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,10 @@ CHANGES ======= +3.3.0 (2019.08.19) +------------------ +- Added `Choices.subset`. + 3.2.0 (2019.06.21) ------------------- - Catch `AttributeError` for deferred abstract fields, fixes GH-331. @@ -413,4 +417,3 @@ CHANGES ----- * Added ``QueryManager`` - diff --git a/docs/utilities.rst b/docs/utilities.rst index b763ba0..3cbbdc7 100644 --- a/docs/utilities.rst +++ b/docs/utilities.rst @@ -84,6 +84,27 @@ instances and other iterable objects that could be converted into Choices: STATUS = GENERIC_CHOICES + [(2, 'featured', _('featured'))] status = models.IntegerField(choices=STATUS, default=STATUS.draft) +Should you wish to provide a subset of choices for a field, for +instance, you have a form class to set some model instance to a failed +state, and only wish to show the user the failed outcomes from which to +select, you can use the ``subset`` method: + +.. code-block:: python + + from model_utils import Choices + + OUTCOMES = Choices( + (0, 'success', _('Successful')), + (1, 'user_cancelled', _('Cancelled by the user')), + (2, 'admin_cancelled', _('Cancelled by an admin')), + ) + FAILED_OUTCOMES = OUTCOMES.subset('user_cancelled', 'admin_cancelled') + +The ``choices`` attribute on the model field can then be set to +``FAILED_OUTCOMES``, thus allowing the subset to be defined in close +proximity to the definition of all the choices, and reused elsewhere as +required. + Field Tracker ============= diff --git a/model_utils/__init__.py b/model_utils/__init__.py index 2fa87c0..9a87de5 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -1,4 +1,4 @@ from .choices import Choices # noqa:F401 from .tracker import FieldTracker, ModelTracker # noqa:F401 -__version__ = '3.2.0' +__version__ = '3.3.0'