From 69d6c6040af67ab948b757ac683fbebdbcb0e399 Mon Sep 17 00:00:00 2001 From: Arseny Sysolyatin Date: Mon, 22 May 2017 20:23:24 +0300 Subject: [PATCH 01/17] 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 fb67a52d0ae2d0ea45e73db971b97948860eda07 Mon Sep 17 00:00:00 2001 From: Adam Dobrawy Date: Sun, 25 Jun 2017 11:26:00 +0200 Subject: [PATCH 02/17] 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 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 03/17] 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 04/17] 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 05/17] 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 06/17] 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 07/17] 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 3efac688ed857c433dcd9225dbe832d8a7470a2b Mon Sep 17 00:00:00 2001 From: Ant Somers Date: Fri, 5 Oct 2018 02:43:52 +0300 Subject: [PATCH 08/17] 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 05671695bb347fea57fcf33c2f0897d918184819 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Sat, 3 Nov 2018 11:12:50 +0600 Subject: [PATCH 09/17] 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 10/17] 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 11/17] 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 12/17] 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 13/17] 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 14/17] 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 15/17] 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 16/17] 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 17/17] 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