mirror of https://github.com/django/django.git
Fixed #25160 -- Moved unsaved model instance data loss check to Model.save()
This mostly reverts5643a3b51b
and81e1a35c36
. Thanks Carl Meyer for review.
This commit is contained in:
parent
12f91f6ebd
commit
5980b05c1f
|
@ -41,8 +41,6 @@ class GenericForeignKey(object):
|
||||||
related_model = None
|
related_model = None
|
||||||
remote_field = None
|
remote_field = None
|
||||||
|
|
||||||
allow_unsaved_instance_assignment = False
|
|
||||||
|
|
||||||
def __init__(self, ct_field='content_type', fk_field='object_id', for_concrete_model=True):
|
def __init__(self, ct_field='content_type', fk_field='object_id', for_concrete_model=True):
|
||||||
self.ct_field = ct_field
|
self.ct_field = ct_field
|
||||||
self.fk_field = fk_field
|
self.fk_field = fk_field
|
||||||
|
@ -253,11 +251,6 @@ class GenericForeignKey(object):
|
||||||
if value is not None:
|
if value is not None:
|
||||||
ct = self.get_content_type(obj=value)
|
ct = self.get_content_type(obj=value)
|
||||||
fk = value._get_pk_val()
|
fk = value._get_pk_val()
|
||||||
if not self.allow_unsaved_instance_assignment and fk is None:
|
|
||||||
raise ValueError(
|
|
||||||
'Cannot assign "%r": "%s" instance isn\'t saved in the database.' %
|
|
||||||
(value, value._meta.object_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
setattr(instance, self.ct_field, ct)
|
setattr(instance, self.ct_field, ct)
|
||||||
setattr(instance, self.fk_field, fk)
|
setattr(instance, self.fk_field, fk)
|
||||||
|
|
|
@ -630,6 +630,30 @@ class Model(six.with_metaclass(ModelBase)):
|
||||||
that the "save" must be an SQL insert or update (or equivalent for
|
that the "save" must be an SQL insert or update (or equivalent for
|
||||||
non-SQL backends), respectively. Normally, they should not be set.
|
non-SQL backends), respectively. Normally, they should not be set.
|
||||||
"""
|
"""
|
||||||
|
# Ensure that a model instance without a PK hasn't been assigned to
|
||||||
|
# a ForeignKey or OneToOneField on this model. If the field is
|
||||||
|
# nullable, allowing the save() would result in silent data loss.
|
||||||
|
for field in self._meta.concrete_fields:
|
||||||
|
if field.is_relation:
|
||||||
|
# If the related field isn't cached, then an instance hasn't
|
||||||
|
# been assigned and there's no need to worry about this check.
|
||||||
|
try:
|
||||||
|
getattr(self, field.get_cache_name())
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
obj = getattr(self, field.name, None)
|
||||||
|
# A pk may have been assigned manually to a model instance not
|
||||||
|
# saved to the database (or auto-generated in a case like
|
||||||
|
# UUIDField), but we allow the save to proceed and rely on the
|
||||||
|
# database to raise an IntegrityError if applicable. If
|
||||||
|
# constraints aren't supported by the database, there's the
|
||||||
|
# unavoidable risk of data corruption.
|
||||||
|
if obj and obj.pk is None:
|
||||||
|
raise ValueError(
|
||||||
|
"save() prohibited to prevent data loss due to "
|
||||||
|
"unsaved related object '%s'." % field.name
|
||||||
|
)
|
||||||
|
|
||||||
using = using or router.db_for_write(self.__class__, instance=self)
|
using = using or router.db_for_write(self.__class__, instance=self)
|
||||||
if force_insert and (force_update or update_fields):
|
if force_insert and (force_update or update_fields):
|
||||||
raise ValueError("Cannot force both insert and updating in model saving.")
|
raise ValueError("Cannot force both insert and updating in model saving.")
|
||||||
|
|
|
@ -515,7 +515,7 @@ class SingleRelatedObjectDescriptor(object):
|
||||||
raise ValueError('Cannot assign "%r": the current database router prevents this relation.' % value)
|
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)
|
related_pk = tuple(getattr(instance, field.attname) for field in self.related.field.foreign_related_fields)
|
||||||
if not self.related.field.allow_unsaved_instance_assignment and None in related_pk:
|
if None in related_pk:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Cannot assign "%r": "%s" instance isn\'t saved in the database.' %
|
'Cannot assign "%r": "%s" instance isn\'t saved in the database.' %
|
||||||
(value, instance._meta.object_name)
|
(value, instance._meta.object_name)
|
||||||
|
@ -685,12 +685,6 @@ class ReverseSingleRelatedObjectDescriptor(object):
|
||||||
# Set the values of the related field.
|
# Set the values of the related field.
|
||||||
else:
|
else:
|
||||||
for lh_field, rh_field in self.field.related_fields:
|
for lh_field, rh_field in self.field.related_fields:
|
||||||
pk = value._get_pk_val()
|
|
||||||
if not self.field.allow_unsaved_instance_assignment and pk is None:
|
|
||||||
raise ValueError(
|
|
||||||
'Cannot assign "%r": "%s" instance isn\'t saved in the database.' %
|
|
||||||
(value, self.field.remote_field.model._meta.object_name)
|
|
||||||
)
|
|
||||||
setattr(instance, lh_field.attname, getattr(value, rh_field.attname))
|
setattr(instance, lh_field.attname, getattr(value, rh_field.attname))
|
||||||
|
|
||||||
# Since we already know what the related object is, seed the related
|
# Since we already know what the related object is, seed the related
|
||||||
|
@ -1601,7 +1595,6 @@ class ForeignObject(RelatedField):
|
||||||
one_to_many = False
|
one_to_many = False
|
||||||
one_to_one = False
|
one_to_one = False
|
||||||
|
|
||||||
allow_unsaved_instance_assignment = False
|
|
||||||
requires_unique_target = True
|
requires_unique_target = True
|
||||||
related_accessor_class = ForeignRelatedObjectsDescriptor
|
related_accessor_class = ForeignRelatedObjectsDescriptor
|
||||||
rel_class = ForeignObjectRel
|
rel_class = ForeignObjectRel
|
||||||
|
|
|
@ -299,13 +299,6 @@ model:
|
||||||
is ``True``. This mirrors the ``for_concrete_model`` argument to
|
is ``True``. This mirrors the ``for_concrete_model`` argument to
|
||||||
:meth:`~django.contrib.contenttypes.models.ContentTypeManager.get_for_model`.
|
:meth:`~django.contrib.contenttypes.models.ContentTypeManager.get_for_model`.
|
||||||
|
|
||||||
.. attribute:: GenericForeignKey.allow_unsaved_instance_assignment
|
|
||||||
|
|
||||||
.. versionadded:: 1.8
|
|
||||||
|
|
||||||
Works analogously to :attr:`ForeignKey.allow_unsaved_instance_assignment
|
|
||||||
<django.db.models.ForeignKey.allow_unsaved_instance_assignment>`.
|
|
||||||
|
|
||||||
.. admonition:: Primary key type compatibility
|
.. admonition:: Primary key type compatibility
|
||||||
|
|
||||||
The "object_id" field doesn't have to be the same type as the
|
The "object_id" field doesn't have to be the same type as the
|
||||||
|
|
|
@ -1383,30 +1383,6 @@ The possible values for :attr:`~ForeignKey.on_delete` are found in
|
||||||
|
|
||||||
If in doubt, leave it to its default of ``True``.
|
If in doubt, leave it to its default of ``True``.
|
||||||
|
|
||||||
.. attribute:: ForeignKey.allow_unsaved_instance_assignment
|
|
||||||
|
|
||||||
.. versionadded:: 1.8
|
|
||||||
|
|
||||||
This flag was added for backwards compatibility as older versions of
|
|
||||||
Django always allowed assigning unsaved model instances.
|
|
||||||
|
|
||||||
Django prevents unsaved model instances from being assigned to a
|
|
||||||
``ForeignKey`` field to prevent accidental data loss (unsaved foreign keys
|
|
||||||
are silently ignored when saving a model instance).
|
|
||||||
|
|
||||||
If you require allowing the assignment of unsaved instances and aren't
|
|
||||||
concerned about the data loss possibility (e.g. you never save the objects
|
|
||||||
to the database), you can disable this check by creating a subclass of the
|
|
||||||
field class and setting its ``allow_unsaved_instance_assignment`` attribute
|
|
||||||
to ``True``. For example::
|
|
||||||
|
|
||||||
class UnsavedForeignKey(models.ForeignKey):
|
|
||||||
# A ForeignKey which can point to an unsaved object
|
|
||||||
allow_unsaved_instance_assignment = True
|
|
||||||
|
|
||||||
class Book(models.Model):
|
|
||||||
author = UnsavedForeignKey(Author, on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
.. _ref-manytomany:
|
.. _ref-manytomany:
|
||||||
|
|
||||||
``ManyToManyField``
|
``ManyToManyField``
|
||||||
|
@ -1607,12 +1583,6 @@ that control how the relationship functions.
|
||||||
|
|
||||||
If in doubt, leave it to its default of ``True``.
|
If in doubt, leave it to its default of ``True``.
|
||||||
|
|
||||||
.. attribute:: ManyToManyField.allow_unsaved_instance_assignment
|
|
||||||
|
|
||||||
.. versionadded:: 1.8
|
|
||||||
|
|
||||||
Works analogously to :attr:`ForeignKey.allow_unsaved_instance_assignment`.
|
|
||||||
|
|
||||||
:class:`ManyToManyField` does not support :attr:`~Field.validators`.
|
:class:`ManyToManyField` does not support :attr:`~Field.validators`.
|
||||||
|
|
||||||
:attr:`~Field.null` has no effect since there is no way to require a
|
:attr:`~Field.null` has no effect since there is no way to require a
|
||||||
|
|
|
@ -27,3 +27,7 @@ Bugfixes
|
||||||
|
|
||||||
* Fixed the recording of squashed migrations when running the ``migrate``
|
* Fixed the recording of squashed migrations when running the ``migrate``
|
||||||
command (:ticket:`25231`).
|
command (:ticket:`25231`).
|
||||||
|
|
||||||
|
* Moved the :ref:`unsaved model instance assignment data loss check
|
||||||
|
<unsaved-model-instance-check-18>` to ``Model.save()`` to allow easier usage
|
||||||
|
of in-memory models (:ticket:`25160`).
|
||||||
|
|
|
@ -685,9 +685,23 @@ This has one backwards incompatible side effect, signal handlers triggered from
|
||||||
these methods are now executed within the method's transaction and any
|
these methods are now executed within the method's transaction and any
|
||||||
exception in a signal handler will prevent the whole operation.
|
exception in a signal handler will prevent the whole operation.
|
||||||
|
|
||||||
|
.. _unsaved-model-instance-check-18:
|
||||||
|
|
||||||
Assigning unsaved objects to relations raises an error
|
Assigning unsaved objects to relations raises an error
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
To more easily allow in-memory usage of models, this change was reverted in
|
||||||
|
Django 1.8.4 and replaced with a check during ``model.save()``. For example::
|
||||||
|
|
||||||
|
>>> book = Book.objects.create(name="Django")
|
||||||
|
>>> book.author = Author(name="John")
|
||||||
|
>>> book.save()
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: save() prohibited to prevent data loss due to unsaved related object 'author'.
|
||||||
|
|
||||||
Assigning unsaved objects to a :class:`~django.db.models.ForeignKey`,
|
Assigning unsaved objects to a :class:`~django.db.models.ForeignKey`,
|
||||||
:class:`~django.contrib.contenttypes.fields.GenericForeignKey`, and
|
:class:`~django.contrib.contenttypes.fields.GenericForeignKey`, and
|
||||||
:class:`~django.db.models.OneToOneField` now raises a :exc:`ValueError`.
|
:class:`~django.db.models.OneToOneField` now raises a :exc:`ValueError`.
|
||||||
|
@ -714,8 +728,8 @@ Now, an error will be raised to prevent data loss::
|
||||||
If you require allowing the assignment of unsaved instances (the old behavior)
|
If you require allowing the assignment of unsaved instances (the old behavior)
|
||||||
and aren't concerned about the data loss possibility (e.g. you never save the
|
and aren't concerned about the data loss possibility (e.g. you never save the
|
||||||
objects to the database), you can disable this check by using the
|
objects to the database), you can disable this check by using the
|
||||||
:attr:`~django.db.models.ForeignKey.allow_unsaved_instance_assignment`
|
``ForeignKey.allow_unsaved_instance_assignment`` attribute. (This attribute was
|
||||||
attribute.
|
removed in 1.8.4 as it's no longer relevant.)
|
||||||
|
|
||||||
Management commands that only accept positional arguments
|
Management commands that only accept positional arguments
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
|
@ -55,19 +55,17 @@ relationship. For example, creating an ``Article`` with unsaved ``Reporter``
|
||||||
raises ``ValueError``::
|
raises ``ValueError``::
|
||||||
|
|
||||||
>>> r3 = Reporter(first_name='John', last_name='Smith', email='john@example.com')
|
>>> r3 = Reporter(first_name='John', last_name='Smith', email='john@example.com')
|
||||||
>>> Article(headline="This is a test", pub_date=date(2005, 7, 27), reporter=r3)
|
>>> Article.objects.create(headline="This is a test", pub_date=date(2005, 7, 27), reporter=r3)
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
ValueError: 'Cannot assign "<Reporter: John Smith>": "Reporter" instance isn't saved in the database.'
|
ValueError: save() prohibited to prevent data loss due to unsaved related object 'reporter'.
|
||||||
|
|
||||||
If you want to disable the unsaved instance check, you can use the
|
.. versionchanged:: 1.8.4
|
||||||
:attr:`~django.db.models.ForeignKey.allow_unsaved_instance_assignment`
|
|
||||||
attribute.
|
|
||||||
|
|
||||||
.. versionchanged:: 1.8
|
Previously, saving an object with unsaved related objects did not raise an
|
||||||
|
error and could result in silent data loss. In 1.8-1.8.3, unsaved model
|
||||||
Previously, assigning unsaved objects did not raise an error and could
|
instances couldn't be assigned to related fields, but this restriction was
|
||||||
result in silent data loss.
|
removed to allow easier usage of in-memory models.
|
||||||
|
|
||||||
Article objects have access to their related Reporter objects::
|
Article objects have access to their related Reporter objects::
|
||||||
|
|
||||||
|
|
|
@ -96,23 +96,17 @@ relationship. For example, creating an ``Restaurant`` with unsaved ``Place``
|
||||||
raises ``ValueError``::
|
raises ``ValueError``::
|
||||||
|
|
||||||
>>> p3 = Place(name='Demon Dogs', address='944 W. Fullerton')
|
>>> p3 = Place(name='Demon Dogs', address='944 W. Fullerton')
|
||||||
>>> Restaurant(place=p3, serves_hot_dogs=True, serves_pizza=False)
|
>>> Restaurant.objects.create(place=p3, serves_hot_dogs=True, serves_pizza=False)
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
ValueError: 'Cannot assign "<Place: Demon Dogs>": "Place" instance isn't saved in the database.'
|
ValueError: save() prohibited to prevent data loss due to unsaved related object 'place'.
|
||||||
>>> p.restaurant = Restaurant(place=p, serves_hot_dogs=True, serves_pizza=False)
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
ValueError: 'Cannot assign "<Restaurant: Demon Dogs the restaurant>": "Restaurant" instance isn't saved in the database.'
|
|
||||||
|
|
||||||
If you want to disable the unsaved instance check, you can use the
|
.. versionchanged:: 1.8.4
|
||||||
:attr:`~django.db.models.ForeignKey.allow_unsaved_instance_assignment`
|
|
||||||
attribute.
|
|
||||||
|
|
||||||
.. versionchanged:: 1.8
|
Previously, saving an object with unsaved related objects did not raise an
|
||||||
|
error and could result in silent data loss. In 1.8-1.8.3, unsaved model
|
||||||
Previously, assigning unsaved objects did not raise an error and could
|
instances couldn't be assigned to related fields, but this restriction was
|
||||||
result in silent data loss.
|
removed to allow easier usage of in-memory models.
|
||||||
|
|
||||||
Restaurant.objects.all() just returns the Restaurants, not the Places. Note
|
Restaurant.objects.all() just returns the Restaurants, not the Places. Note
|
||||||
that there are two restaurants - Ace Hardware the Restaurant was created in the
|
that there are two restaurants - Ace Hardware the Restaurant was created in the
|
||||||
|
|
|
@ -12,7 +12,7 @@ from django.contrib.admin.utils import (
|
||||||
label_for_field, lookup_field, quote,
|
label_for_field, lookup_field, quote,
|
||||||
)
|
)
|
||||||
from django.db import DEFAULT_DB_ALIAS, models
|
from django.db import DEFAULT_DB_ALIAS, models
|
||||||
from django.test import TestCase, override_settings
|
from django.test import SimpleTestCase, TestCase, override_settings
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.formats import localize
|
from django.utils.formats import localize
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
@ -94,7 +94,7 @@ class NestedObjectsTests(TestCase):
|
||||||
n.collect([Vehicle.objects.first()])
|
n.collect([Vehicle.objects.first()])
|
||||||
|
|
||||||
|
|
||||||
class UtilsTests(TestCase):
|
class UtilsTests(SimpleTestCase):
|
||||||
|
|
||||||
empty_value = '-empty-'
|
empty_value = '-empty-'
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ class UtilsTests(TestCase):
|
||||||
|
|
||||||
simple_function = lambda obj: SIMPLE_FUNCTION
|
simple_function = lambda obj: SIMPLE_FUNCTION
|
||||||
|
|
||||||
site_obj = Site.objects.create(domain=SITE_NAME)
|
site_obj = Site(domain=SITE_NAME)
|
||||||
article = Article(
|
article = Article(
|
||||||
site=site_obj,
|
site=site_obj,
|
||||||
title=TITLE_TEXT,
|
title=TITLE_TEXT,
|
||||||
|
|
|
@ -236,54 +236,6 @@ class GenericForeignKeyTests(IsolatedModelsTestCase):
|
||||||
errors = checks.run_checks()
|
errors = checks.run_checks()
|
||||||
self.assertEqual(errors, ['performed!'])
|
self.assertEqual(errors, ['performed!'])
|
||||||
|
|
||||||
def test_unsaved_instance_on_generic_foreign_key(self):
|
|
||||||
"""
|
|
||||||
#10811 -- Assigning an unsaved object to GenericForeignKey
|
|
||||||
should raise an exception.
|
|
||||||
"""
|
|
||||||
class Model(models.Model):
|
|
||||||
content_type = models.ForeignKey(ContentType, models.SET_NULL, null=True)
|
|
||||||
object_id = models.PositiveIntegerField(null=True)
|
|
||||||
content_object = GenericForeignKey('content_type', 'object_id')
|
|
||||||
|
|
||||||
author = Author(name='Author')
|
|
||||||
model = Model()
|
|
||||||
model.content_object = None # no error here as content_type allows None
|
|
||||||
with self.assertRaisesMessage(ValueError,
|
|
||||||
'Cannot assign "%r": "%s" instance isn\'t saved in the database.'
|
|
||||||
% (author, author._meta.object_name)):
|
|
||||||
model.content_object = author # raised ValueError here as author is unsaved
|
|
||||||
|
|
||||||
author.save()
|
|
||||||
model.content_object = author # no error because the instance is saved
|
|
||||||
|
|
||||||
def test_unsaved_instance_on_generic_foreign_key_allowed_when_wanted(self):
|
|
||||||
"""
|
|
||||||
#24495 - Assigning an unsaved object to a GenericForeignKey
|
|
||||||
should be allowed when the allow_unsaved_instance_assignment
|
|
||||||
attribute has been set to True.
|
|
||||||
"""
|
|
||||||
class UnsavedGenericForeignKey(GenericForeignKey):
|
|
||||||
# A GenericForeignKey which can point to an unsaved object
|
|
||||||
allow_unsaved_instance_assignment = True
|
|
||||||
|
|
||||||
class Band(models.Model):
|
|
||||||
name = models.CharField(max_length=50)
|
|
||||||
|
|
||||||
class BandMember(models.Model):
|
|
||||||
band_ct = models.ForeignKey(ContentType, models.CASCADE)
|
|
||||||
band_id = models.PositiveIntegerField()
|
|
||||||
band = UnsavedGenericForeignKey('band_ct', 'band_id')
|
|
||||||
first_name = models.CharField(max_length=50)
|
|
||||||
last_name = models.CharField(max_length=50)
|
|
||||||
|
|
||||||
beatles = Band(name='The Beatles')
|
|
||||||
john = BandMember(first_name='John', last_name='Lennon')
|
|
||||||
# This should not raise an exception as the GenericForeignKey between
|
|
||||||
# member and band has allow_unsaved_instance_assignment=True.
|
|
||||||
john.band = beatles
|
|
||||||
self.assertEqual(john.band, beatles)
|
|
||||||
|
|
||||||
|
|
||||||
class GenericRelationshipTests(IsolatedModelsTestCase):
|
class GenericRelationshipTests(IsolatedModelsTestCase):
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ from django import forms
|
||||||
from django.contrib.contenttypes.forms import generic_inlineformset_factory
|
from django.contrib.contenttypes.forms import generic_inlineformset_factory
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import FieldError
|
from django.core.exceptions import FieldError
|
||||||
|
from django.db import IntegrityError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.test import SimpleTestCase, TestCase
|
from django.test import SimpleTestCase, TestCase
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
@ -486,6 +487,15 @@ class GenericRelationsTests(TestCase):
|
||||||
with self.assertRaisesMessage(FieldError, msg):
|
with self.assertRaisesMessage(FieldError, msg):
|
||||||
TaggedItem.objects.get(content_object='')
|
TaggedItem.objects.get(content_object='')
|
||||||
|
|
||||||
|
def test_unsaved_instance_on_generic_foreign_key(self):
|
||||||
|
"""
|
||||||
|
Assigning an unsaved object to GenericForeignKey should raise an
|
||||||
|
exception on model.save().
|
||||||
|
"""
|
||||||
|
quartz = Mineral(name="Quartz", hardness=7)
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
TaggedItem.objects.create(tag="shiny", content_object=quartz)
|
||||||
|
|
||||||
|
|
||||||
class CustomWidget(forms.TextInput):
|
class CustomWidget(forms.TextInput):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -163,31 +163,6 @@ class ManyToOneTests(TestCase):
|
||||||
self.assertFalse(hasattr(self.r2.article_set, 'remove'))
|
self.assertFalse(hasattr(self.r2.article_set, 'remove'))
|
||||||
self.assertFalse(hasattr(self.r2.article_set, 'clear'))
|
self.assertFalse(hasattr(self.r2.article_set, 'clear'))
|
||||||
|
|
||||||
def test_assign_unsaved_check_override(self):
|
|
||||||
"""
|
|
||||||
#24495 - Assigning an unsaved object to a ForeignKey
|
|
||||||
should be allowed when the allow_unsaved_instance_assignment
|
|
||||||
attribute has been set to True.
|
|
||||||
"""
|
|
||||||
class UnsavedForeignKey(models.ForeignKey):
|
|
||||||
# A ForeignKey which can point to an unsaved object
|
|
||||||
allow_unsaved_instance_assignment = True
|
|
||||||
|
|
||||||
class Band(models.Model):
|
|
||||||
name = models.CharField(max_length=50)
|
|
||||||
|
|
||||||
class BandMember(models.Model):
|
|
||||||
band = UnsavedForeignKey(Band, models.CASCADE)
|
|
||||||
first_name = models.CharField(max_length=50)
|
|
||||||
last_name = models.CharField(max_length=50)
|
|
||||||
|
|
||||||
beatles = Band(name='The Beatles')
|
|
||||||
john = BandMember(first_name='John', last_name='Lennon')
|
|
||||||
# This should not raise an exception as the ForeignKey between member
|
|
||||||
# and band has allow_unsaved_instance_assignment=True.
|
|
||||||
john.band = beatles
|
|
||||||
self.assertEqual(john.band, beatles)
|
|
||||||
|
|
||||||
def test_selects(self):
|
def test_selects(self):
|
||||||
self.r.article_set.create(headline="John's second story",
|
self.r.article_set.create(headline="John's second story",
|
||||||
pub_date=datetime.date(2005, 7, 29))
|
pub_date=datetime.date(2005, 7, 29))
|
||||||
|
@ -567,15 +542,13 @@ class ManyToOneTests(TestCase):
|
||||||
|
|
||||||
# Creation using keyword argument and unsaved related instance (#8070).
|
# Creation using keyword argument and unsaved related instance (#8070).
|
||||||
p = Parent()
|
p = Parent()
|
||||||
with self.assertRaisesMessage(ValueError,
|
msg = "save() prohibited to prevent data loss due to unsaved related object 'parent'."
|
||||||
'Cannot assign "%r": "%s" instance isn\'t saved in the database.'
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
% (p, Child.parent.field.remote_field.model._meta.object_name)):
|
Child.objects.create(parent=p)
|
||||||
Child(parent=p)
|
|
||||||
|
|
||||||
with self.assertRaisesMessage(ValueError,
|
msg = "save() prohibited to prevent data loss due to unsaved related object 'parent'."
|
||||||
'Cannot assign "%r": "%s" instance isn\'t saved in the database.'
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
% (p, Child.parent.field.remote_field.model._meta.object_name)):
|
ToFieldChild.objects.create(parent=p)
|
||||||
ToFieldChild(parent=p)
|
|
||||||
|
|
||||||
# Creation using attname keyword argument and an id will cause the
|
# Creation using attname keyword argument and an id will cause the
|
||||||
# related object to be fetched.
|
# related object to be fetched.
|
||||||
|
|
|
@ -2,8 +2,10 @@ import json
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.core import exceptions, serializers
|
from django.core import exceptions, serializers
|
||||||
from django.db import models
|
from django.db import IntegrityError, models
|
||||||
from django.test import SimpleTestCase, TestCase
|
from django.test import (
|
||||||
|
SimpleTestCase, TestCase, TransactionTestCase, skipUnlessDBFeature,
|
||||||
|
)
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
NullableUUIDModel, PrimaryKeyUUIDModel, RelatedToUUIDModel, UUIDGrandchild,
|
NullableUUIDModel, PrimaryKeyUUIDModel, RelatedToUUIDModel, UUIDGrandchild,
|
||||||
|
@ -158,3 +160,14 @@ class TestAsPrimaryKey(TestCase):
|
||||||
def test_two_level_foreign_keys(self):
|
def test_two_level_foreign_keys(self):
|
||||||
# exercises ForeignKey.get_db_prep_value()
|
# exercises ForeignKey.get_db_prep_value()
|
||||||
UUIDGrandchild().save()
|
UUIDGrandchild().save()
|
||||||
|
|
||||||
|
|
||||||
|
class TestAsPrimaryKeyTransactionTests(TransactionTestCase):
|
||||||
|
# Need a TransactionTestCase to avoid deferring FK constraint checking.
|
||||||
|
available_apps = ['model_fields']
|
||||||
|
|
||||||
|
@skipUnlessDBFeature('supports_foreign_keys')
|
||||||
|
def test_unsaved_fk(self):
|
||||||
|
u1 = PrimaryKeyUUIDModel()
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
RelatedToUUIDModel.objects.create(uuid_fk=u1)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import IntegrityError, connection, models, transaction
|
from django.db import IntegrityError, connection, transaction
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
|
@ -134,41 +134,9 @@ class OneToOneTests(TestCase):
|
||||||
should raise an exception.
|
should raise an exception.
|
||||||
"""
|
"""
|
||||||
place = Place(name='User', address='London')
|
place = Place(name='User', address='London')
|
||||||
with self.assertRaisesMessage(ValueError,
|
msg = "save() prohibited to prevent data loss due to unsaved related object 'place'."
|
||||||
'Cannot assign "%r": "%s" instance isn\'t saved in the database.'
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
% (place, Restaurant.place.field.remote_field.model._meta.object_name)):
|
|
||||||
Restaurant.objects.create(place=place, serves_hot_dogs=True, serves_pizza=False)
|
Restaurant.objects.create(place=place, serves_hot_dogs=True, serves_pizza=False)
|
||||||
bar = UndergroundBar()
|
|
||||||
p = Place(name='User', address='London')
|
|
||||||
with self.assertRaisesMessage(ValueError,
|
|
||||||
'Cannot assign "%r": "%s" instance isn\'t saved in the database.'
|
|
||||||
% (bar, p._meta.object_name)):
|
|
||||||
p.undergroundbar = bar
|
|
||||||
|
|
||||||
def test_unsaved_object_check_override(self):
|
|
||||||
"""
|
|
||||||
#24495 - Assigning an unsaved object to a OneToOneField
|
|
||||||
should be allowed when the allow_unsaved_instance_assignment
|
|
||||||
attribute has been set to True.
|
|
||||||
"""
|
|
||||||
class UnsavedOneToOneField(models.OneToOneField):
|
|
||||||
# A OneToOneField which can point to an unsaved object
|
|
||||||
allow_unsaved_instance_assignment = True
|
|
||||||
|
|
||||||
class Band(models.Model):
|
|
||||||
name = models.CharField(max_length=50)
|
|
||||||
|
|
||||||
class BandManager(models.Model):
|
|
||||||
band = UnsavedOneToOneField(Band, models.CASCADE)
|
|
||||||
first_name = models.CharField(max_length=50)
|
|
||||||
last_name = models.CharField(max_length=50)
|
|
||||||
|
|
||||||
band = Band(name='The Beatles')
|
|
||||||
manager = BandManager(first_name='Brian', last_name='Epstein')
|
|
||||||
# This should not raise an exception as the OneToOneField between
|
|
||||||
# manager and band has allow_unsaved_instance_assignment=True.
|
|
||||||
manager.band = band
|
|
||||||
self.assertEqual(manager.band, band)
|
|
||||||
|
|
||||||
def test_reverse_relationship_cache_cascade(self):
|
def test_reverse_relationship_cache_cascade(self):
|
||||||
"""
|
"""
|
||||||
|
@ -249,6 +217,11 @@ class OneToOneTests(TestCase):
|
||||||
r = Restaurant(place=p)
|
r = Restaurant(place=p)
|
||||||
self.assertIs(r.place, p)
|
self.assertIs(r.place, p)
|
||||||
|
|
||||||
|
# Creation using keyword argument and unsaved related instance (#8070).
|
||||||
|
p = Place()
|
||||||
|
r = Restaurant(place=p)
|
||||||
|
self.assertTrue(r.place is p)
|
||||||
|
|
||||||
# Creation using attname keyword argument and an id will cause the related
|
# Creation using attname keyword argument and an id will cause the related
|
||||||
# object to be fetched.
|
# object to be fetched.
|
||||||
p = Place.objects.get(name="Demon Dogs")
|
p = Place.objects.get(name="Demon Dogs")
|
||||||
|
@ -392,8 +365,12 @@ class OneToOneTests(TestCase):
|
||||||
"""
|
"""
|
||||||
p = Place()
|
p = Place()
|
||||||
b = UndergroundBar.objects.create()
|
b = UndergroundBar.objects.create()
|
||||||
|
msg = (
|
||||||
|
'Cannot assign "<UndergroundBar: UndergroundBar object>": "Place" '
|
||||||
|
'instance isn\'t saved in the database.'
|
||||||
|
)
|
||||||
with self.assertNumQueries(0):
|
with self.assertNumQueries(0):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
p.undergroundbar = b
|
p.undergroundbar = b
|
||||||
|
|
||||||
def test_nullable_o2o_delete(self):
|
def test_nullable_o2o_delete(self):
|
||||||
|
|
Loading…
Reference in New Issue