Commit f99aada0 authored by Brian May's avatar Brian May

Import django-model-utils_2.3.1.orig.tar.gz

parent 258d7071
......@@ -7,3 +7,5 @@ Django-*.egg
*.pyc
htmlcov/
docs/_build/
.idea/
.eggs/
language: python
python:
- 2.6
- 2.7
- 3.2
- 3.3
python: 2.7
env:
- DJANGO=Django==1.4.10 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
- TOXENV=py26-django14
- TOXENV=py26-django15
- TOXENV=py26-django16
- TOXENV=py27-django14
- TOXENV=py27-django15
- 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
- TOXENV=py34-django17
- TOXENV=py34-django18
- TOXENV=py34-django_trunk
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: 3.2
env: DJANGO=Django==1.4.10 SOUTH=1
- python: 3.3
env: DJANGO=Django==1.4.10 SOUTH=1
include:
- python: 2.7
env: DJANGO=Django==1.5.5 SOUTH=0
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
ad-m <github.com/ad-m>
Alejandro Varas <alej0varas@gmail.com>
Alex Orange <crazycasta@gmail.com>
Andy Freeland <andy@andyfreeland.net>
Bram Boogaard <b.boogaard@auto-interactive.nl>
Carl Meyer <carl@dirtcircle.com>
Curtis Maloney <curtis@tinbrain.net>
Den Lesnov
Dmytro Kyrychuk <dmytro.kyrychuck@gmail.com>
Donald Stufft <donald.stufft@gmail.com>
Douglas Meehan <dmeehan@gmail.com>
Facundo Gaich <facugaich@gmail.com>
......@@ -21,7 +24,9 @@ Michael van Tellingen <michaelvantellingen@gmail.com>
Mikhail Silonov <silonov.pro>
Patryk Zawadzki <patrys@room-303.com>
Paul McLanahan <paul@mclanahan.net>
Philipp Steinhardt <steinhardt@myvision.de>
Rinat Shigapov <rinatshigapov@gmail.com>
Rodney Folz <rodney@rodneyfolz.com>
rsenkbeil <github.com/rsenkbeil>
Ryan Kaskel <dev@ryankaskel.com>
Simon Meers <simon@simonmeers.com>
......
CHANGES
=======
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.
2.3 (2015.07.17)
----------------
* Keep track of deferred fields on model instance instead of on
FieldInstanceTracker instance. Fixes accessing deferred fields for multiple
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.
* Add German translations.
* Django 1.8 compatibility.
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)
------------------
* 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)
------------------
* Add support for Django's built-in migrations to ``MonitorField`` and
``StatusField``.
* ``PassThroughManager`` now has support for seeing exposed methods via
``dir``, allowing `IPython`_ tab completion to be useful. Merge of GH-104,
fixes GH-55.
* 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.
.. _IPython: http://ipython.org/
2.0.3 (2014.03.19)
-------------------
......
......@@ -22,11 +22,31 @@ 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 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
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
-------
......
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
......
......@@ -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
......@@ -13,3 +13,9 @@ docs: documentation
documentation:
python setup.py build_sphinx
messages:
python translations.py make
compilemessages:
python translations.py compile
......@@ -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.
......
......@@ -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__))
......
......@@ -212,3 +212,10 @@ Now you have a "pass through 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``.)
......@@ -210,3 +210,16 @@ An example using the model specified above:
>>> a.body = 'First post!'
>>> a.title_tracker.changed()
{'title': None}
Checking changes using signals
------------------------------
The field tracker methods may also be used in ``pre_save`` and ``post_save``
signal handlers to identify field changes on model save.
.. NOTE::
Due to the implementation of ``FieldTracker``, ``post_save`` signal
handlers relying on field tracker methods should only be registered after
model creation.
from .choices import Choices
from .tracker import FieldTracker, ModelTracker
__version__ = '2.0.3'
__version__ = '2.3.1'
......@@ -70,6 +70,11 @@ class StatusField(models.CharField):
self._choices = [(0, 'dummy')]
super(StatusField, self).contribute_to_class(cls, name)
def deconstruct(self):
name, path, args, kwargs = super(StatusField, self).deconstruct()
kwargs['no_check_for_status'] = True
return name, path, args, kwargs
class MonitorField(models.DateTimeField):
"""
......@@ -113,6 +118,13 @@ class MonitorField(models.DateTimeField):
self._save_initial(model_instance.__class__, model_instance)
return super(MonitorField, self).pre_save(model_instance, add)
def deconstruct(self):
name, path, args, kwargs = super(MonitorField, self).deconstruct()
kwargs['monitor'] = self.monitor
if self.when is not None:
kwargs['when'] = self.when
return name, path, args, kwargs
SPLIT_MARKER = getattr(settings, 'SPLIT_MARKER', '<!-- split -->')
......@@ -217,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'] = True
return name, path, args, kwargs
# allow South to handle these fields smoothly
try:
......
# This file is distributed under the same license as the django-model-utils package.
#
# Translators:
# Philipp Steinhardt <steinhardt@myvision.de>, 2015.
msgid ""
msgstr ""
"Project-Id-Version: django-model-utils\n"
"Report-Msgid-Bugs-To: \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 <steinhardt@myvision.de>\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"
#: 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"
......@@ -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
......@@ -244,6 +247,20 @@ class PassThroughManagerMixin(object):
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()
......@@ -257,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):
......
......@@ -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,7 @@ 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):
return field_name in [f.attname for f in model_class._meta.local_fields]
......@@ -12,6 +12,7 @@ from django.db import models
from django.db.models.fields import FieldDoesNotExist
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
......@@ -31,6 +32,12 @@ from model_utils.tests.models import (
InheritanceManagerTestChild3, StatusFieldChoicesName)
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<!-- split -->\n\nsome more")
......@@ -1235,6 +1242,40 @@ class PassThroughManagerTests(TestCase):
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')
......@@ -1444,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'))
......@@ -1458,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):
......
......@@ -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
......
......@@ -16,10 +16,11 @@ DEFAULT_SETTINGS = dict(
"ENGINE": "django.db.backends.sqlite3"
}
},
SILENCED_SYSTEM_CHECKS=["1_7.W001"],
)
def runtests(*test_args):
def runtests():
if not settings.configured:
settings.configure(**DEFAULT_SETTINGS)
......@@ -27,14 +28,19 @@ def runtests(*test_args):
if hasattr(django, 'setup'):
django.setup()
if not test_args:
test_args = ['tests']
parent = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, parent)
from django.test.simple import DjangoTestSuiteRunner
failures = DjangoTestSuiteRunner(
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']
failures = runner_class(
verbosity=1, interactive=True, failfast=False).run_tests(test_args)
sys.exit(failures)
......
......@@ -13,7 +13,6 @@ def get_version():
if line.startswith('__version__ ='):
return line.split('=')[1].strip().strip('"\'')