Fixed #8070 -- Cache related objects passed to Model init as keyword arguments. Also:
* Model init no longer performs a database query to refetch the related objects it is passed. * Model init now caches unsaved related objects correctly, too. (Previously, accessing the field would raise `DoesNotExist` error for `null=False` fields.) * Added tests for assigning `None` to `null=True` `ForeignKey` fields (refs #6886). git-svn-id: http://code.djangoproject.com/svn/django/trunk@8185 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
b5c5e8b4c0
commit
2db4b13480
|
@ -200,6 +200,7 @@ class Model(object):
|
||||||
# keywords, or default.
|
# keywords, or default.
|
||||||
|
|
||||||
for field in fields_iter:
|
for field in fields_iter:
|
||||||
|
rel_obj = None
|
||||||
if kwargs:
|
if kwargs:
|
||||||
if isinstance(field.rel, ManyToOneRel):
|
if isinstance(field.rel, ManyToOneRel):
|
||||||
try:
|
try:
|
||||||
|
@ -216,17 +217,18 @@ class Model(object):
|
||||||
# pass in "None" for related objects if it's allowed.
|
# pass in "None" for related objects if it's allowed.
|
||||||
if rel_obj is None and field.null:
|
if rel_obj is None and field.null:
|
||||||
val = None
|
val = None
|
||||||
else:
|
|
||||||
try:
|
|
||||||
val = getattr(rel_obj, field.rel.get_related_field().attname)
|
|
||||||
except AttributeError:
|
|
||||||
raise TypeError("Invalid value: %r should be a %s instance, not a %s" %
|
|
||||||
(field.name, field.rel.to, type(rel_obj)))
|
|
||||||
else:
|
else:
|
||||||
val = kwargs.pop(field.attname, field.get_default())
|
val = kwargs.pop(field.attname, field.get_default())
|
||||||
else:
|
else:
|
||||||
val = field.get_default()
|
val = field.get_default()
|
||||||
setattr(self, field.attname, val)
|
# If we got passed a related instance, set it using the field.name
|
||||||
|
# instead of field.attname (e.g. "user" instead of "user_id") so
|
||||||
|
# that the object gets properly cached (and type checked) by the
|
||||||
|
# RelatedObjectDescriptor.
|
||||||
|
if rel_obj:
|
||||||
|
setattr(self, field.name, rel_obj)
|
||||||
|
else:
|
||||||
|
setattr(self, field.attname, val)
|
||||||
|
|
||||||
if kwargs:
|
if kwargs:
|
||||||
for prop in kwargs.keys():
|
for prop in kwargs.keys():
|
||||||
|
|
|
@ -46,8 +46,12 @@ __test__ = {'API_TESTS':"""
|
||||||
|
|
||||||
# Article objects have access to their related Reporter objects.
|
# Article objects have access to their related Reporter objects.
|
||||||
>>> r = a.reporter
|
>>> r = a.reporter
|
||||||
|
|
||||||
|
# These are strings instead of unicode strings because that's what was used in
|
||||||
|
# the creation of this reporter (and we haven't refreshed the data from the
|
||||||
|
# database, which always returns unicode strings).
|
||||||
>>> r.first_name, r.last_name
|
>>> r.first_name, r.last_name
|
||||||
(u'John', u'Smith')
|
('John', 'Smith')
|
||||||
|
|
||||||
# Create an Article via the Reporter object.
|
# Create an Article via the Reporter object.
|
||||||
>>> new_article = r.article_set.create(headline="John's second story", pub_date=datetime(2005, 7, 29))
|
>>> new_article = r.article_set.create(headline="John's second story", pub_date=datetime(2005, 7, 29))
|
||||||
|
@ -176,7 +180,7 @@ False
|
||||||
[<Article: John's second story>, <Article: Paul's story>, <Article: This is a test>]
|
[<Article: John's second story>, <Article: Paul's story>, <Article: This is a test>]
|
||||||
|
|
||||||
# You can also use a queryset instead of a literal list of instances.
|
# You can also use a queryset instead of a literal list of instances.
|
||||||
# The queryset must be reduced to a list of values using values(),
|
# The queryset must be reduced to a list of values using values(),
|
||||||
# then converted into a query
|
# then converted into a query
|
||||||
>>> Article.objects.filter(reporter__in=Reporter.objects.filter(first_name='John').values('pk').query).distinct()
|
>>> Article.objects.filter(reporter__in=Reporter.objects.filter(first_name='John').values('pk').query).distinct()
|
||||||
[<Article: John's second story>, <Article: This is a test>]
|
[<Article: John's second story>, <Article: This is a test>]
|
||||||
|
|
|
@ -55,31 +55,36 @@ __test__ = {'API_TESTS':"""
|
||||||
<Child: Child object>
|
<Child: Child object>
|
||||||
|
|
||||||
#
|
#
|
||||||
# Tests of ForeignKey assignment and the related-object cache (see #6886)
|
# Tests of ForeignKey assignment and the related-object cache (see #6886).
|
||||||
#
|
#
|
||||||
>>> p = Parent.objects.create(name="Parent")
|
>>> p = Parent.objects.create(name="Parent")
|
||||||
>>> c = Child.objects.create(name="Child", parent=p)
|
>>> c = Child.objects.create(name="Child", parent=p)
|
||||||
|
|
||||||
# Look up the object again so that we get a "fresh" object
|
# Look up the object again so that we get a "fresh" object.
|
||||||
>>> c = Child.objects.get(name="Child")
|
>>> c = Child.objects.get(name="Child")
|
||||||
>>> p = c.parent
|
>>> p = c.parent
|
||||||
|
|
||||||
# Accessing the related object again returns the exactly same object
|
# Accessing the related object again returns the exactly same object.
|
||||||
>>> c.parent is p
|
>>> c.parent is p
|
||||||
True
|
True
|
||||||
|
|
||||||
# But if we kill the cache, we get a new object
|
# But if we kill the cache, we get a new object.
|
||||||
>>> del c._parent_cache
|
>>> del c._parent_cache
|
||||||
>>> c.parent is p
|
>>> c.parent is p
|
||||||
False
|
False
|
||||||
|
|
||||||
# Assigning a new object results in that object getting cached immediately
|
# Assigning a new object results in that object getting cached immediately.
|
||||||
>>> p2 = Parent.objects.create(name="Parent 2")
|
>>> p2 = Parent.objects.create(name="Parent 2")
|
||||||
>>> c.parent = p2
|
>>> c.parent = p2
|
||||||
>>> c.parent is p2
|
>>> c.parent is p2
|
||||||
True
|
True
|
||||||
|
|
||||||
# Assigning None fails: Child.parent is null=False
|
# Assigning None succeeds if field is null=True.
|
||||||
|
>>> p.bestchild = None
|
||||||
|
>>> p.bestchild is None
|
||||||
|
True
|
||||||
|
|
||||||
|
# Assigning None fails: Child.parent is null=False.
|
||||||
>>> c.parent = None
|
>>> c.parent = None
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
|
@ -91,8 +96,31 @@ Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
ValueError: Cannot assign "<First: First object>": "Child.parent" must be a "Parent" instance.
|
ValueError: Cannot assign "<First: First object>": "Child.parent" must be a "Parent" instance.
|
||||||
|
|
||||||
# Test of multiple ForeignKeys to the same model (bug #7125)
|
# Creation using keyword argument should cache the related object.
|
||||||
|
>>> p = Parent.objects.get(name="Parent")
|
||||||
|
>>> c = Child(parent=p)
|
||||||
|
>>> c.parent is p
|
||||||
|
True
|
||||||
|
|
||||||
|
# Creation using keyword argument and unsaved related instance (#8070).
|
||||||
|
>>> p = Parent()
|
||||||
|
>>> c = Child(parent=p)
|
||||||
|
>>> c.parent is p
|
||||||
|
True
|
||||||
|
|
||||||
|
# Creation using attname keyword argument and an id will cause the related
|
||||||
|
# object to be fetched.
|
||||||
|
>>> p = Parent.objects.get(name="Parent")
|
||||||
|
>>> c = Child(parent_id=p.id)
|
||||||
|
>>> c.parent is p
|
||||||
|
False
|
||||||
|
>>> c.parent == p
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Test of multiple ForeignKeys to the same model (bug #7125).
|
||||||
|
#
|
||||||
>>> c1 = Category.objects.create(name='First')
|
>>> c1 = Category.objects.create(name='First')
|
||||||
>>> c2 = Category.objects.create(name='Second')
|
>>> c2 = Category.objects.create(name='Second')
|
||||||
>>> c3 = Category.objects.create(name='Third')
|
>>> c3 = Category.objects.create(name='Third')
|
||||||
|
|
|
@ -22,6 +22,10 @@ class Bar(models.Model):
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return u"%s the bar" % self.place.name
|
return u"%s the bar" % self.place.name
|
||||||
|
|
||||||
|
class UndergroundBar(models.Model):
|
||||||
|
place = models.OneToOneField(Place, null=True)
|
||||||
|
serves_cocktails = models.BooleanField()
|
||||||
|
|
||||||
class Favorites(models.Model):
|
class Favorites(models.Model):
|
||||||
name = models.CharField(max_length = 50)
|
name = models.CharField(max_length = 50)
|
||||||
restaurants = models.ManyToManyField(Restaurant)
|
restaurants = models.ManyToManyField(Restaurant)
|
||||||
|
@ -42,7 +46,7 @@ __test__ = {'API_TESTS':"""
|
||||||
>>> f.restaurants.all()
|
>>> f.restaurants.all()
|
||||||
[<Restaurant: Demon Dogs the restaurant>]
|
[<Restaurant: Demon Dogs the restaurant>]
|
||||||
|
|
||||||
# Regression test for #7173: Check that the name of the cache for the
|
# Regression test for #7173: Check that the name of the cache for the
|
||||||
# reverse object is correct.
|
# reverse object is correct.
|
||||||
>>> b = Bar(place=p1, serves_cocktails=False)
|
>>> b = Bar(place=p1, serves_cocktails=False)
|
||||||
>>> b.save()
|
>>> b.save()
|
||||||
|
@ -53,7 +57,7 @@ __test__ = {'API_TESTS':"""
|
||||||
|
|
||||||
#
|
#
|
||||||
# Regression test for #6886 (the related-object cache)
|
# Regression test for #6886 (the related-object cache)
|
||||||
#
|
#
|
||||||
|
|
||||||
# Look up the objects again so that we get "fresh" objects
|
# Look up the objects again so that we get "fresh" objects
|
||||||
>>> p = Place.objects.get(name="Demon Dogs")
|
>>> p = Place.objects.get(name="Demon Dogs")
|
||||||
|
@ -76,6 +80,12 @@ False
|
||||||
>>> p.restaurant is r2
|
>>> p.restaurant is r2
|
||||||
True
|
True
|
||||||
|
|
||||||
|
# Assigning None succeeds if field is null=True.
|
||||||
|
>>> ug_bar = UndergroundBar.objects.create(place=p, serves_cocktails=False)
|
||||||
|
>>> ug_bar.place = None
|
||||||
|
>>> ug_bar.place is None
|
||||||
|
True
|
||||||
|
|
||||||
# Assigning None fails: Place.restaurant is null=False
|
# Assigning None fails: Place.restaurant is null=False
|
||||||
>>> p.restaurant = None
|
>>> p.restaurant = None
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
|
@ -88,4 +98,25 @@ Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
ValueError: Cannot assign "<Place: Demon Dogs the place>": "Place.restaurant" must be a "Restaurant" instance.
|
ValueError: Cannot assign "<Place: Demon Dogs the place>": "Place.restaurant" must be a "Restaurant" instance.
|
||||||
|
|
||||||
|
# Creation using keyword argument should cache the related object.
|
||||||
|
>>> p = Place.objects.get(name="Demon Dogs")
|
||||||
|
>>> r = Restaurant(place=p)
|
||||||
|
>>> r.place is p
|
||||||
|
True
|
||||||
|
|
||||||
|
# Creation using keyword argument and unsaved related instance (#8070).
|
||||||
|
>>> p = Place()
|
||||||
|
>>> r = Restaurant(place=p)
|
||||||
|
>>> r.place is p
|
||||||
|
True
|
||||||
|
|
||||||
|
# Creation using attname keyword argument and an id will cause the related
|
||||||
|
# object to be fetched.
|
||||||
|
>>> p = Place.objects.get(name="Demon Dogs")
|
||||||
|
>>> r = Restaurant(place_id=p.id)
|
||||||
|
>>> r.place is p
|
||||||
|
False
|
||||||
|
>>> r.place == p
|
||||||
|
True
|
||||||
|
|
||||||
"""}
|
"""}
|
||||||
|
|
Loading…
Reference in New Issue