Merge pull request #191 from jarekwg/master

Add support for Django 1.9
This commit is contained in:
Carl Meyer 2015-10-28 17:11:29 -06:00
commit fda1fd2cf3
9 changed files with 61 additions and 303 deletions

View file

@ -12,6 +12,7 @@ env:
- TOXENV=py27-django16
- TOXENV=py27-django17
- TOXENV=py27-django18
- TOXENV=py27-django19
- TOXENV=py27-django_trunk
- TOXENV=py32-django15
- TOXENV=py32-django16
@ -25,6 +26,7 @@ env:
- TOXENV=py33-django_trunk
- TOXENV=py34-django17
- TOXENV=py34-django18
- TOXENV=py34-django19
- TOXENV=py34-django_trunk
install:

View file

@ -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):

View file

@ -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)
@ -224,91 +223,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

View file

@ -1,10 +1,14 @@
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, \

View file

@ -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)

View file

@ -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,80 +201,10 @@ 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()
mutable = MutableField()
mutable = MutableField(default=None)
tracker = FieldTracker()
@ -319,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()

View file

@ -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,
@ -872,11 +871,10 @@ class InheritanceManagerUsingModelsTests(TestCase):
"""
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')
@ -1206,115 +1204,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
@ -1502,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,

View file

@ -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,

View file

@ -3,7 +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},
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