mirror of
https://github.com/Hopiu/django-model-utils.git
synced 2026-03-16 20:00:23 +00:00
fix conflict
This commit is contained in:
commit
af1824e200
26 changed files with 426 additions and 51 deletions
|
|
@ -1,5 +1,2 @@
|
|||
[run]
|
||||
source = model_utils
|
||||
omit = .*
|
||||
tests/*
|
||||
*/_*
|
||||
include = model_utils/*.py
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
| Jarek Glowacki <github.com/jarekwg>
|
||||
| Javier García Sogo <jgsogo@gmail.com>
|
||||
| Jeff Elmore <jeffelmore.org>
|
||||
| Jonathan Sundqvist <jonathan@argpar.se>
|
||||
| Keryn Knight <kerynknight.com>
|
||||
| Martey Dodoo <martey+django-model-utils@mobolic.com>
|
||||
| Matthew Schinckel <matt@schinckel.net>
|
||||
|
|
@ -47,3 +48,4 @@
|
|||
| Lucas Wiman <lucas.wiman@gmail.com>
|
||||
| Jack Cushman <jcushman@law.harvard.edu>
|
||||
| Zach Cheung <kuroro.zhang@gmail.com>
|
||||
| Daniel Andrlik <daniel@andrlik.org>
|
||||
|
|
|
|||
|
|
@ -7,12 +7,20 @@ master (unreleased)
|
|||
- Fix `FieldTracker.has_changed()` and `FieldTracker.previous()` to return
|
||||
correct responses for deferred fields.
|
||||
- Add Simplified Chinese translations.
|
||||
- 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
|
||||
- Fix patched `save` in FieldTracker
|
||||
|
||||
3.1.2 (2018.05.09)
|
||||
------------------
|
||||
* 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)
|
||||
------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
11
README.rst
11
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/
|
||||
|
||||
|
|
@ -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
|
||||
============
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
BIN
model_utils/locale/cs/LC_MESSAGES/django.mo
Normal file
BIN
model_utils/locale/cs/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
46
model_utils/locale/cs/LC_MESSAGES/django.po
Normal file
46
model_utils/locale/cs/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Czech translations of django-model-utils
|
||||
#
|
||||
# This file is distributed under the same license as the django-model-utils package.
|
||||
#
|
||||
# Translators:
|
||||
# ------------
|
||||
# Václav Dohnal <vaclav.dohnal@gmail.com>, 2018.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-model-utils\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"
|
||||
"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 <vaclav.dohnal@gmail.com>\n"
|
||||
"Language-Team: N/A\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"
|
||||
BIN
model_utils/locale/ru/LC_MESSAGES/django.mo
Normal file
BIN
model_utils/locale/ru/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
43
model_utils/locale/ru/LC_MESSAGES/django.po
Normal file
43
model_utils/locale/ru/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# This file is distributed under the same license as the django-model-utils package.
|
||||
#
|
||||
# Translators:
|
||||
# Arseny Sysolyatin <arseny.sysolyatin@gmail.com>, 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 <arseny.sysolyatin@gmail.com>\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 "статус изменен"
|
||||
|
|
@ -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):
|
||||
|
|
@ -98,16 +97,16 @@ 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):
|
||||
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 +188,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 +304,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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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, *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
|
||||
|
|
@ -253,7 +253,7 @@ class FieldTracker(object):
|
|||
)
|
||||
return ret
|
||||
|
||||
instance.save = save
|
||||
model.save = save
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
pytest==3.3.1
|
||||
pytest-django==3.1.2
|
||||
psycopg2==2.7.6.1
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,22 @@
|
|||
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", ""),
|
||||
"HOST": os.environ.get("DJANGO_DATABASE_HOST_POSTGRES", ""),
|
||||
},
|
||||
}
|
||||
SECRET_KEY = 'dummy'
|
||||
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
@ -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)
|
||||
|
|
@ -639,6 +644,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'
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
@ -451,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)
|
||||
|
|
|
|||
38
tests/test_managers/test_join_manager.py
Normal file
38
tests/test_managers/test_join_manager.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
7
tox.ini
7
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue