Fixed #27629 -- Added router.allow_relation() calls for assignments between unsaved model instances.

This commit is contained in:
Stefan R. Filipek 2018-05-10 20:42:44 -04:00 committed by Tim Graham
parent 9c4ea63e87
commit a5a2ceeb45
5 changed files with 39 additions and 9 deletions

View File

@ -763,6 +763,7 @@ answer newbie questions, and generally made Django that much better:
Stanislaus Madueke
Stanislav Karpov <work@stkrp.ru>
starrynight <cmorgh@gmail.com>
Stefan R. Filipek
Stefane Fermgier <sf@fermigier.com>
Stefano Rivera <stefano@rivera.za.net>
Stéphane Raimbault <stephane.raimbault@gmail.com>

View File

@ -205,11 +205,10 @@ class ForwardManyToOneDescriptor:
elif value is not None:
if instance._state.db is None:
instance._state.db = router.db_for_write(instance.__class__, instance=value)
elif value._state.db is None:
if value._state.db is None:
value._state.db = router.db_for_write(value.__class__, instance=instance)
elif value._state.db is not None and instance._state.db is not None:
if not router.allow_relation(value, instance):
raise ValueError('Cannot assign "%r": the current database router prevents this relation.' % value)
if not router.allow_relation(value, instance):
raise ValueError('Cannot assign "%r": the current database router prevents this relation.' % value)
remote_field = self.field.remote_field
# If we're setting the value of a OneToOneField to None, we need to clear
@ -451,11 +450,10 @@ class ReverseOneToOneDescriptor:
else:
if instance._state.db is None:
instance._state.db = router.db_for_write(instance.__class__, instance=value)
elif value._state.db is None:
if value._state.db is None:
value._state.db = router.db_for_write(value.__class__, instance=instance)
elif value._state.db is not None and instance._state.db is not None:
if not router.allow_relation(value, instance):
raise ValueError('Cannot assign "%r": the current database router prevents this relation.' % value)
if not router.allow_relation(value, instance):
raise ValueError('Cannot assign "%r": the current database router prevents this relation.' % value)
related_pk = tuple(getattr(instance, field.attname) for field in self.related.field.foreign_related_fields)
# Set the value of the related field to the value of the related object's related field

View File

@ -415,6 +415,9 @@ Miscellaneous
* ``QuerySet.raw()`` now caches its results like regular querysets. Use
``iterator()`` if you don't want caching.
* The database router :meth:`allow_relation` method is called in more cases.
Improperly written routers may need to be updated accordingly.
.. _deprecated-features-2.1:
Features deprecated in 2.1

View File

@ -319,7 +319,10 @@ class OtherRouter:
return self.db_for_read(model, **hints)
def allow_relation(self, obj1, obj2, **hints):
return None
# ContentType objects are created during a post-migrate signal while
# performing fixture teardown using the default database alias and
# don't abide by the database specified by this router.
return True
def allow_migrate(self, db, app_label, **hints):
return True

View File

@ -2082,3 +2082,28 @@ class RouteForWriteTestCase(TestCase):
self.assertEqual(e.mode, RouterUsed.WRITE)
self.assertEqual(e.model, Book)
self.assertEqual(e.hints, {'instance': auth})
class NoRelationRouter:
"""Disallow all relations."""
def allow_relation(self, obj1, obj2, **hints):
return False
@override_settings(DATABASE_ROUTERS=[NoRelationRouter()])
class RelationAssignmentTests(TestCase):
"""allow_relation() is called with unsaved model instances."""
multi_db = True
router_prevents_msg = 'the current database router prevents this relation'
def test_foreign_key_relation(self):
person = Person(name='Someone')
pet = Pet()
with self.assertRaisesMessage(ValueError, self.router_prevents_msg):
pet.owner = person
def test_reverse_one_to_one_relation(self):
user = User(username='Someone', password='fake_hash')
profile = UserProfile()
with self.assertRaisesMessage(ValueError, self.router_prevents_msg):
user.userprofile = profile