Fixed #13724: Corrected routing of write queries involving managers.
Previously, if a database request spanned a related object manager, the first manager encountered would cause a request to the router, and this would bind all subsequent queries to the same database returned by the router. Unfortunately, the first router query would be performed using a read request to the router, resulting in bad routing information being used if the subsequent query was actually a write. This change defers the call to the router until the final query is acutally made. It includes a small *BACKWARDS INCOMPATIBILITY* on an edge case - see the release notes for details. Thanks to Paul Collins (@paulcollinsiii) for the excellent debugging work and patch.
This commit is contained in:
parent
12ca312e1b
commit
9595183d03
|
@ -159,9 +159,8 @@ class SingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjectDescri
|
||||||
def is_cached(self, instance):
|
def is_cached(self, instance):
|
||||||
return hasattr(instance, self.cache_name)
|
return hasattr(instance, self.cache_name)
|
||||||
|
|
||||||
def get_queryset(self, **db_hints):
|
def get_queryset(self, **hints):
|
||||||
db = router.db_for_read(self.related.model, **db_hints)
|
return self.related.model._base_manager.db_manager(hints=hints)
|
||||||
return self.related.model._base_manager.using(db)
|
|
||||||
|
|
||||||
def get_prefetch_queryset(self, instances):
|
def get_prefetch_queryset(self, instances):
|
||||||
rel_obj_attr = attrgetter(self.related.field.attname)
|
rel_obj_attr = attrgetter(self.related.field.attname)
|
||||||
|
@ -256,15 +255,14 @@ class ReverseSingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjec
|
||||||
def is_cached(self, instance):
|
def is_cached(self, instance):
|
||||||
return hasattr(instance, self.cache_name)
|
return hasattr(instance, self.cache_name)
|
||||||
|
|
||||||
def get_queryset(self, **db_hints):
|
def get_queryset(self, **hints):
|
||||||
db = router.db_for_read(self.field.rel.to, **db_hints)
|
rel_mgr = self.field.rel.to._default_manager.db_manager(hints=hints)
|
||||||
rel_mgr = self.field.rel.to._default_manager
|
|
||||||
# If the related manager indicates that it should be used for
|
# If the related manager indicates that it should be used for
|
||||||
# related fields, respect that.
|
# related fields, respect that.
|
||||||
if getattr(rel_mgr, 'use_for_related_fields', False):
|
if getattr(rel_mgr, 'use_for_related_fields', False):
|
||||||
return rel_mgr.using(db)
|
return rel_mgr
|
||||||
else:
|
else:
|
||||||
return QuerySet(self.field.rel.to).using(db)
|
return QuerySet(self.field.rel.to, hints=hints)
|
||||||
|
|
||||||
def get_prefetch_queryset(self, instances):
|
def get_prefetch_queryset(self, instances):
|
||||||
rel_obj_attr = self.field.get_foreign_related_value
|
rel_obj_attr = self.field.get_foreign_related_value
|
||||||
|
@ -385,8 +383,12 @@ def create_foreign_related_manager(superclass, rel_field, rel_model):
|
||||||
return self.instance._prefetched_objects_cache[rel_field.related_query_name()]
|
return self.instance._prefetched_objects_cache[rel_field.related_query_name()]
|
||||||
except (AttributeError, KeyError):
|
except (AttributeError, KeyError):
|
||||||
db = self._db or router.db_for_read(self.model, instance=self.instance)
|
db = self._db or router.db_for_read(self.model, instance=self.instance)
|
||||||
qs = super(RelatedManager, self).get_queryset().using(db).filter(**self.core_filters)
|
|
||||||
empty_strings_as_null = connections[db].features.interprets_empty_strings_as_nulls
|
empty_strings_as_null = connections[db].features.interprets_empty_strings_as_nulls
|
||||||
|
qs = super(RelatedManager, self).get_queryset()
|
||||||
|
qs._add_hints(instance=self.instance)
|
||||||
|
if self._db:
|
||||||
|
qs = qs.using(self._db)
|
||||||
|
qs = qs.filter(**self.core_filters)
|
||||||
for field in rel_field.foreign_related_fields:
|
for field in rel_field.foreign_related_fields:
|
||||||
val = getattr(self.instance, field.attname)
|
val = getattr(self.instance, field.attname)
|
||||||
if val is None or (val == '' and empty_strings_as_null):
|
if val is None or (val == '' and empty_strings_as_null):
|
||||||
|
@ -398,9 +400,12 @@ def create_foreign_related_manager(superclass, rel_field, rel_model):
|
||||||
rel_obj_attr = rel_field.get_local_related_value
|
rel_obj_attr = rel_field.get_local_related_value
|
||||||
instance_attr = rel_field.get_foreign_related_value
|
instance_attr = rel_field.get_foreign_related_value
|
||||||
instances_dict = dict((instance_attr(inst), inst) for inst in instances)
|
instances_dict = dict((instance_attr(inst), inst) for inst in instances)
|
||||||
db = self._db or router.db_for_read(self.model, instance=instances[0])
|
|
||||||
query = {'%s__in' % rel_field.name: instances}
|
query = {'%s__in' % rel_field.name: instances}
|
||||||
qs = super(RelatedManager, self).get_queryset().using(db).filter(**query)
|
qs = super(RelatedManager, self).get_queryset()
|
||||||
|
qs._add_hints(instance=instances[0])
|
||||||
|
if self._db:
|
||||||
|
qs = qs.using(self._db)
|
||||||
|
qs = qs.filter(**query)
|
||||||
# Since we just bypassed this class' get_queryset(), we must manage
|
# Since we just bypassed this class' get_queryset(), we must manage
|
||||||
# the reverse relation manually.
|
# the reverse relation manually.
|
||||||
for rel_obj in qs:
|
for rel_obj in qs:
|
||||||
|
@ -545,14 +550,21 @@ def create_many_related_manager(superclass, rel):
|
||||||
try:
|
try:
|
||||||
return self.instance._prefetched_objects_cache[self.prefetch_cache_name]
|
return self.instance._prefetched_objects_cache[self.prefetch_cache_name]
|
||||||
except (AttributeError, KeyError):
|
except (AttributeError, KeyError):
|
||||||
db = self._db or router.db_for_read(self.instance.__class__, instance=self.instance)
|
qs = super(ManyRelatedManager, self).get_queryset()
|
||||||
return super(ManyRelatedManager, self).get_queryset().using(db)._next_is_sticky().filter(**self.core_filters)
|
qs._add_hints(instance=self.instance)
|
||||||
|
if self._db:
|
||||||
|
qs = qs.using(self._db)
|
||||||
|
return qs._next_is_sticky().filter(**self.core_filters)
|
||||||
|
|
||||||
def get_prefetch_queryset(self, instances):
|
def get_prefetch_queryset(self, instances):
|
||||||
instance = instances[0]
|
instance = instances[0]
|
||||||
db = self._db or router.db_for_read(instance.__class__, instance=instance)
|
db = self._db or router.db_for_read(instance.__class__, instance=instance)
|
||||||
query = {'%s__in' % self.query_field_name: instances}
|
query = {'%s__in' % self.query_field_name: instances}
|
||||||
qs = super(ManyRelatedManager, self).get_queryset().using(db)._next_is_sticky().filter(**query)
|
qs = super(ManyRelatedManager, self).get_queryset()
|
||||||
|
qs._add_hints(instance=instance)
|
||||||
|
if self._db:
|
||||||
|
qs = qs.using(db)
|
||||||
|
qs = qs._next_is_sticky().filter(**query)
|
||||||
|
|
||||||
# M2M: need to annotate the query in order to get the primary model
|
# M2M: need to annotate the query in order to get the primary model
|
||||||
# that the secondary model was actually related to. We know that
|
# that the secondary model was actually related to. We know that
|
||||||
|
|
|
@ -68,6 +68,7 @@ class BaseManager(six.with_metaclass(RenameManagerMethods)):
|
||||||
self.model = None
|
self.model = None
|
||||||
self._inherited = False
|
self._inherited = False
|
||||||
self._db = None
|
self._db = None
|
||||||
|
self._hints = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_queryset_methods(cls, queryset_class):
|
def _get_queryset_methods(cls, queryset_class):
|
||||||
|
@ -144,21 +145,22 @@ class BaseManager(six.with_metaclass(RenameManagerMethods)):
|
||||||
mgr._inherited = True
|
mgr._inherited = True
|
||||||
return mgr
|
return mgr
|
||||||
|
|
||||||
def db_manager(self, using):
|
def db_manager(self, using=None, hints=None):
|
||||||
obj = copy.copy(self)
|
obj = copy.copy(self)
|
||||||
obj._db = using
|
obj._db = using or self._db
|
||||||
|
obj._hints = hints or self._hints
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def db(self):
|
def db(self):
|
||||||
return self._db or router.db_for_read(self.model)
|
return self._db or router.db_for_read(self.model, **self._hints)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""
|
"""
|
||||||
Returns a new QuerySet object. Subclasses can override this method to
|
Returns a new QuerySet object. Subclasses can override this method to
|
||||||
easily customize the behavior of the Manager.
|
easily customize the behavior of the Manager.
|
||||||
"""
|
"""
|
||||||
return self._queryset_class(self.model, using=self._db)
|
return self._queryset_class(self.model, using=self._db, hints=self._hints)
|
||||||
|
|
||||||
def all(self):
|
def all(self):
|
||||||
# We can't proxy this method through the `QuerySet` like we do for the
|
# We can't proxy this method through the `QuerySet` like we do for the
|
||||||
|
|
|
@ -47,9 +47,10 @@ class QuerySet(object):
|
||||||
Represents a lazy database lookup for a set of objects.
|
Represents a lazy database lookup for a set of objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, model=None, query=None, using=None):
|
def __init__(self, model=None, query=None, using=None, hints=None):
|
||||||
self.model = model
|
self.model = model
|
||||||
self._db = using
|
self._db = using
|
||||||
|
self._hints = hints or {}
|
||||||
self.query = query or sql.Query(self.model)
|
self.query = query or sql.Query(self.model)
|
||||||
self._result_cache = None
|
self._result_cache = None
|
||||||
self._sticky_filter = False
|
self._sticky_filter = False
|
||||||
|
@ -904,8 +905,8 @@ class QuerySet(object):
|
||||||
def db(self):
|
def db(self):
|
||||||
"Return the database that will be used if this query is executed now"
|
"Return the database that will be used if this query is executed now"
|
||||||
if self._for_write:
|
if self._for_write:
|
||||||
return self._db or router.db_for_write(self.model)
|
return self._db or router.db_for_write(self.model, **self._hints)
|
||||||
return self._db or router.db_for_read(self.model)
|
return self._db or router.db_for_read(self.model, **self._hints)
|
||||||
|
|
||||||
###################
|
###################
|
||||||
# PRIVATE METHODS #
|
# PRIVATE METHODS #
|
||||||
|
@ -955,7 +956,7 @@ class QuerySet(object):
|
||||||
query = self.query.clone()
|
query = self.query.clone()
|
||||||
if self._sticky_filter:
|
if self._sticky_filter:
|
||||||
query.filter_is_sticky = True
|
query.filter_is_sticky = True
|
||||||
c = klass(model=self.model, query=query, using=self._db)
|
c = klass(model=self.model, query=query, using=self._db, hints=self._hints)
|
||||||
c._for_write = self._for_write
|
c._for_write = self._for_write
|
||||||
c._prefetch_related_lookups = self._prefetch_related_lookups[:]
|
c._prefetch_related_lookups = self._prefetch_related_lookups[:]
|
||||||
c._known_related_objects = self._known_related_objects
|
c._known_related_objects = self._known_related_objects
|
||||||
|
@ -1025,6 +1026,14 @@ class QuerySet(object):
|
||||||
# empty" result.
|
# empty" result.
|
||||||
value_annotation = True
|
value_annotation = True
|
||||||
|
|
||||||
|
def _add_hints(self, **hints):
|
||||||
|
"""
|
||||||
|
Update hinting information for later use by Routers
|
||||||
|
"""
|
||||||
|
# If there is any hinting information, add it to what we already know.
|
||||||
|
# If we have a new hint for an existing key, overwrite with the new value.
|
||||||
|
self._hints.update(hints)
|
||||||
|
|
||||||
|
|
||||||
class InstanceCheckMeta(type):
|
class InstanceCheckMeta(type):
|
||||||
def __instancecheck__(self, instance):
|
def __instancecheck__(self, instance):
|
||||||
|
@ -1485,10 +1494,11 @@ class RawQuerySet(object):
|
||||||
annotated model instances.
|
annotated model instances.
|
||||||
"""
|
"""
|
||||||
def __init__(self, raw_query, model=None, query=None, params=None,
|
def __init__(self, raw_query, model=None, query=None, params=None,
|
||||||
translations=None, using=None):
|
translations=None, using=None, hints=None):
|
||||||
self.raw_query = raw_query
|
self.raw_query = raw_query
|
||||||
self.model = model
|
self.model = model
|
||||||
self._db = using
|
self._db = using
|
||||||
|
self._hints = hints or {}
|
||||||
self.query = query or sql.RawQuery(sql=raw_query, using=self.db, params=params)
|
self.query = query or sql.RawQuery(sql=raw_query, using=self.db, params=params)
|
||||||
self.params = params or ()
|
self.params = params or ()
|
||||||
self.translations = translations or {}
|
self.translations = translations or {}
|
||||||
|
@ -1572,7 +1582,7 @@ class RawQuerySet(object):
|
||||||
@property
|
@property
|
||||||
def db(self):
|
def db(self):
|
||||||
"Return the database that will be used if this query is executed now"
|
"Return the database that will be used if this query is executed now"
|
||||||
return self._db or router.db_for_read(self.model)
|
return self._db or router.db_for_read(self.model, **self._hints)
|
||||||
|
|
||||||
def using(self, alias):
|
def using(self, alias):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -368,6 +368,20 @@ For apps with migrations, ``allow_migrate`` will now get passed
|
||||||
without custom attributes, methods or managers. Make sure your ``allow_migrate``
|
without custom attributes, methods or managers. Make sure your ``allow_migrate``
|
||||||
methods are only referring to fields or other items in ``model._meta``.
|
methods are only referring to fields or other items in ``model._meta``.
|
||||||
|
|
||||||
|
Passing ``None`` to ``Manager.db_manager()``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
In previous versions of Django, it was possible to use
|
||||||
|
``db_manager(using=None)`` on a model manager instance to obtain a manager
|
||||||
|
instance using default routing behavior, overriding any manually specified
|
||||||
|
database routing. In Django 1.7, a value of ``None`` passed to db_manager will
|
||||||
|
produce a router that *retains* any manually assigned database routing -- the
|
||||||
|
manager will *not* be reset. This was necessary to resolve an inconsistency in
|
||||||
|
the way routing information cascaded over joins. See `Ticket #13724`_ for more
|
||||||
|
details.
|
||||||
|
|
||||||
|
.. _Ticket #13724: https://code.djangoproject.com/ticket/13724
|
||||||
|
|
||||||
pytz may be required
|
pytz may be required
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -1902,3 +1902,244 @@ class MigrateTestCase(TestCase):
|
||||||
router.routers = old_routers
|
router.routers = old_routers
|
||||||
|
|
||||||
self.assertEqual(cts.count(), 0)
|
self.assertEqual(cts.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RouterUsed(Exception):
|
||||||
|
WRITE = 'write'
|
||||||
|
|
||||||
|
def __init__(self, mode, model, hints):
|
||||||
|
self.mode = mode
|
||||||
|
self.model = model
|
||||||
|
self.hints = hints
|
||||||
|
|
||||||
|
|
||||||
|
class RouteForWriteTestCase(TestCase):
|
||||||
|
multi_db = True
|
||||||
|
RAISE_MSG = 'Db for write called'
|
||||||
|
|
||||||
|
class WriteCheckRouter(object):
|
||||||
|
def db_for_write(self, model, **hints):
|
||||||
|
raise RouterUsed(mode=RouterUsed.WRITE, model=model, hints=hints)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self._old_rtrs = router.routers
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
router.routers = self._old_rtrs
|
||||||
|
|
||||||
|
def enable_router(self):
|
||||||
|
router.routers = [RouteForWriteTestCase.WriteCheckRouter()]
|
||||||
|
|
||||||
|
def test_fk_delete(self):
|
||||||
|
owner = Person.objects.create(name='Someone')
|
||||||
|
pet = Pet.objects.create(name='fido', owner=owner)
|
||||||
|
self.enable_router()
|
||||||
|
try:
|
||||||
|
pet.owner.delete()
|
||||||
|
self.fail('db_for_write() not invoked on router')
|
||||||
|
except RouterUsed, e:
|
||||||
|
self.assertEqual(e.mode, RouterUsed.WRITE)
|
||||||
|
self.assertEqual(e.model, Person)
|
||||||
|
self.assertEqual(e.hints, {'instance': owner})
|
||||||
|
|
||||||
|
def test_reverse_fk_delete(self):
|
||||||
|
owner = Person.objects.create(name='Someone')
|
||||||
|
to_del_qs = owner.pet_set.all()
|
||||||
|
self.enable_router()
|
||||||
|
try:
|
||||||
|
to_del_qs.delete()
|
||||||
|
self.fail('db_for_write() not invoked on router')
|
||||||
|
except RouterUsed, e:
|
||||||
|
self.assertEqual(e.mode, RouterUsed.WRITE)
|
||||||
|
self.assertEqual(e.model, Pet)
|
||||||
|
self.assertEqual(e.hints, {'instance': owner})
|
||||||
|
|
||||||
|
def test_reverse_fk_get_or_create(self):
|
||||||
|
owner = Person.objects.create(name='Someone')
|
||||||
|
self.enable_router()
|
||||||
|
try:
|
||||||
|
owner.pet_set.get_or_create(name='fido')
|
||||||
|
self.fail('db_for_write() not invoked on router')
|
||||||
|
except RouterUsed, e:
|
||||||
|
self.assertEqual(e.mode, RouterUsed.WRITE)
|
||||||
|
self.assertEqual(e.model, Pet)
|
||||||
|
self.assertEqual(e.hints, {'instance': owner})
|
||||||
|
|
||||||
|
def test_reverse_fk_update(self):
|
||||||
|
owner = Person.objects.create(name='Someone')
|
||||||
|
pet = Pet.objects.create(name='fido', owner=owner)
|
||||||
|
self.enable_router()
|
||||||
|
try:
|
||||||
|
owner.pet_set.update(name='max')
|
||||||
|
self.fail('db_for_write() not invoked on router')
|
||||||
|
except RouterUsed, e:
|
||||||
|
self.assertEqual(e.mode, RouterUsed.WRITE)
|
||||||
|
self.assertEqual(e.model, Pet)
|
||||||
|
self.assertEqual(e.hints, {'instance': owner})
|
||||||
|
|
||||||
|
def test_m2m_add(self):
|
||||||
|
auth = Person.objects.create(name='Someone')
|
||||||
|
book = Book.objects.create(title="Pro Django",
|
||||||
|
published=datetime.date(2008, 12, 16))
|
||||||
|
self.enable_router()
|
||||||
|
try:
|
||||||
|
book.authors.add(auth)
|
||||||
|
self.fail('db_for_write() not invoked on router')
|
||||||
|
except RouterUsed, e:
|
||||||
|
self.assertEqual(e.mode, RouterUsed.WRITE)
|
||||||
|
self.assertEqual(e.model, Book.authors.through)
|
||||||
|
self.assertEqual(e.hints, {'instance': book})
|
||||||
|
|
||||||
|
def test_m2m_clear(self):
|
||||||
|
auth = Person.objects.create(name='Someone')
|
||||||
|
book = Book.objects.create(title="Pro Django",
|
||||||
|
published=datetime.date(2008, 12, 16))
|
||||||
|
book.authors.add(auth)
|
||||||
|
self.enable_router()
|
||||||
|
try:
|
||||||
|
book.authors.clear()
|
||||||
|
self.fail('db_for_write() not invoked on router')
|
||||||
|
except RouterUsed, e:
|
||||||
|
self.assertEqual(e.mode, RouterUsed.WRITE)
|
||||||
|
self.assertEqual(e.model, Book.authors.through)
|
||||||
|
self.assertEqual(e.hints, {'instance': book})
|
||||||
|
|
||||||
|
def test_m2m_delete(self):
|
||||||
|
auth = Person.objects.create(name='Someone')
|
||||||
|
book = Book.objects.create(title="Pro Django",
|
||||||
|
published=datetime.date(2008, 12, 16))
|
||||||
|
book.authors.add(auth)
|
||||||
|
self.enable_router()
|
||||||
|
try:
|
||||||
|
book.authors.all().delete()
|
||||||
|
self.fail('db_for_write() not invoked on router')
|
||||||
|
except RouterUsed, e:
|
||||||
|
self.assertEqual(e.mode, RouterUsed.WRITE)
|
||||||
|
self.assertEqual(e.model, Person)
|
||||||
|
self.assertEqual(e.hints, {'instance': book})
|
||||||
|
|
||||||
|
def test_m2m_get_or_create(self):
|
||||||
|
auth = Person.objects.create(name='Someone')
|
||||||
|
book = Book.objects.create(title="Pro Django",
|
||||||
|
published=datetime.date(2008, 12, 16))
|
||||||
|
self.enable_router()
|
||||||
|
try:
|
||||||
|
book.authors.get_or_create(name='Someone else')
|
||||||
|
self.fail('db_for_write() not invoked on router')
|
||||||
|
except RouterUsed, e:
|
||||||
|
self.assertEqual(e.mode, RouterUsed.WRITE)
|
||||||
|
self.assertEqual(e.model, Book)
|
||||||
|
self.assertEqual(e.hints, {'instance': book})
|
||||||
|
|
||||||
|
def test_m2m_remove(self):
|
||||||
|
auth = Person.objects.create(name='Someone')
|
||||||
|
book = Book.objects.create(title="Pro Django",
|
||||||
|
published=datetime.date(2008, 12, 16))
|
||||||
|
book.authors.add(auth)
|
||||||
|
self.enable_router()
|
||||||
|
self.assertRaisesMessage(AttributeError, self.RAISE_MSG, )
|
||||||
|
try:
|
||||||
|
book.authors.remove(auth)
|
||||||
|
self.fail('db_for_write() not invoked on router')
|
||||||
|
except RouterUsed, e:
|
||||||
|
self.assertEqual(e.mode, RouterUsed.WRITE)
|
||||||
|
self.assertEqual(e.model, Book.authors.through)
|
||||||
|
self.assertEqual(e.hints, {'instance': book})
|
||||||
|
|
||||||
|
def test_m2m_update(self):
|
||||||
|
auth = Person.objects.create(name='Someone')
|
||||||
|
book = Book.objects.create(title="Pro Django",
|
||||||
|
published=datetime.date(2008, 12, 16))
|
||||||
|
book.authors.add(auth)
|
||||||
|
self.enable_router()
|
||||||
|
try:
|
||||||
|
book.authors.all().update(name='Different')
|
||||||
|
self.fail('db_for_write() not invoked on router')
|
||||||
|
except RouterUsed, e:
|
||||||
|
self.assertEqual(e.mode, RouterUsed.WRITE)
|
||||||
|
self.assertEqual(e.model, Person)
|
||||||
|
self.assertEqual(e.hints, {'instance': book})
|
||||||
|
|
||||||
|
def test_reverse_m2m_add(self):
|
||||||
|
auth = Person.objects.create(name='Someone')
|
||||||
|
book = Book.objects.create(title="Pro Django",
|
||||||
|
published=datetime.date(2008, 12, 16))
|
||||||
|
self.enable_router()
|
||||||
|
try:
|
||||||
|
auth.book_set.add(book)
|
||||||
|
self.fail('db_for_write() not invoked on router')
|
||||||
|
except RouterUsed, e:
|
||||||
|
self.assertEqual(e.mode, RouterUsed.WRITE)
|
||||||
|
self.assertEqual(e.model, Book.authors.through)
|
||||||
|
self.assertEqual(e.hints, {'instance': auth})
|
||||||
|
|
||||||
|
def test_reverse_m2m_clear(self):
|
||||||
|
auth = Person.objects.create(name='Someone')
|
||||||
|
book = Book.objects.create(title="Pro Django",
|
||||||
|
published=datetime.date(2008, 12, 16))
|
||||||
|
book.authors.add(auth)
|
||||||
|
self.enable_router()
|
||||||
|
try:
|
||||||
|
auth.book_set.clear()
|
||||||
|
self.fail('db_for_write() not invoked on router')
|
||||||
|
except RouterUsed, e:
|
||||||
|
self.assertEqual(e.mode, RouterUsed.WRITE)
|
||||||
|
self.assertEqual(e.model, Book.authors.through)
|
||||||
|
self.assertEqual(e.hints, {'instance': auth})
|
||||||
|
|
||||||
|
def test_reverse_m2m_delete(self):
|
||||||
|
auth = Person.objects.create(name='Someone')
|
||||||
|
book = Book.objects.create(title="Pro Django",
|
||||||
|
published=datetime.date(2008, 12, 16))
|
||||||
|
book.authors.add(auth)
|
||||||
|
self.enable_router()
|
||||||
|
try:
|
||||||
|
auth.book_set.all().delete()
|
||||||
|
self.fail('db_for_write() not invoked on router')
|
||||||
|
except RouterUsed, e:
|
||||||
|
self.assertEqual(e.mode, RouterUsed.WRITE)
|
||||||
|
self.assertEqual(e.model, Book)
|
||||||
|
self.assertEqual(e.hints, {'instance': auth})
|
||||||
|
|
||||||
|
def test_reverse_m2m_get_or_create(self):
|
||||||
|
auth = Person.objects.create(name='Someone')
|
||||||
|
book = Book.objects.create(title="Pro Django",
|
||||||
|
published=datetime.date(2008, 12, 16))
|
||||||
|
self.enable_router()
|
||||||
|
try:
|
||||||
|
auth.book_set.get_or_create(title="New Book", published=datetime.datetime.now())
|
||||||
|
self.fail('db_for_write() not invoked on router')
|
||||||
|
except RouterUsed, e:
|
||||||
|
self.assertEqual(e.mode, RouterUsed.WRITE)
|
||||||
|
self.assertEqual(e.model, Person)
|
||||||
|
self.assertEqual(e.hints, {'instance': auth})
|
||||||
|
|
||||||
|
def test_reverse_m2m_remove(self):
|
||||||
|
auth = Person.objects.create(name='Someone')
|
||||||
|
book = Book.objects.create(title="Pro Django",
|
||||||
|
published=datetime.date(2008, 12, 16))
|
||||||
|
book.authors.add(auth)
|
||||||
|
self.enable_router()
|
||||||
|
try:
|
||||||
|
auth.book_set.remove(book)
|
||||||
|
self.fail('db_for_write() not invoked on router')
|
||||||
|
except RouterUsed, e:
|
||||||
|
self.assertEqual(e.mode, RouterUsed.WRITE)
|
||||||
|
self.assertEqual(e.model, Book.authors.through)
|
||||||
|
self.assertEqual(e.hints, {'instance': auth})
|
||||||
|
|
||||||
|
def test_reverse_m2m_update(self):
|
||||||
|
auth = Person.objects.create(name='Someone')
|
||||||
|
book = Book.objects.create(title="Pro Django",
|
||||||
|
published=datetime.date(2008, 12, 16))
|
||||||
|
book.authors.add(auth)
|
||||||
|
self.enable_router()
|
||||||
|
try:
|
||||||
|
auth.book_set.all().update(title='Different')
|
||||||
|
self.fail('db_for_write() not invoked on router')
|
||||||
|
except RouterUsed, e:
|
||||||
|
self.assertEqual(e.mode, RouterUsed.WRITE)
|
||||||
|
self.assertEqual(e.model, Book)
|
||||||
|
self.assertEqual(e.hints, {'instance': auth})
|
||||||
|
|
Loading…
Reference in New Issue