Skip to content

Commit

Permalink
Fixed #25160 -- Moved unsaved model instance data loss check to Model…
Browse files Browse the repository at this point in the history
….save()

This mostly reverts 5643a3b and
81e1a35.

Thanks Carl Meyer for review.
  • Loading branch information
timgraham committed Aug 10, 2015
1 parent 12f91f6 commit 5980b05
Show file tree
Hide file tree
Showing 15 changed files with 107 additions and 199 deletions.
7 changes: 0 additions & 7 deletions django/contrib/contenttypes/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ class GenericForeignKey(object):
related_model = None
remote_field = None

allow_unsaved_instance_assignment = False

def __init__(self, ct_field='content_type', fk_field='object_id', for_concrete_model=True):
self.ct_field = ct_field
self.fk_field = fk_field
Expand Down Expand Up @@ -253,11 +251,6 @@ def __set__(self, instance, value):
if value is not None:
ct = self.get_content_type(obj=value)
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.fk_field, fk)
Expand Down
24 changes: 24 additions & 0 deletions django/db/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,30 @@ def save(self, force_insert=False, force_update=False, using=None,
that the "save" must be an SQL insert or update (or equivalent for
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)
if force_insert and (force_update or update_fields):
raise ValueError("Cannot force both insert and updating in model saving.")
Expand Down
9 changes: 1 addition & 8 deletions django/db/models/fields/related.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@ def __set__(self, instance, 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)
if not self.related.field.allow_unsaved_instance_assignment and None in related_pk:
if None in related_pk:
raise ValueError(
'Cannot assign "%r": "%s" instance isn\'t saved in the database.' %
(value, instance._meta.object_name)
Expand Down Expand Up @@ -685,12 +685,6 @@ def __set__(self, instance, value):
# Set the values of the related field.
else:
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))

# Since we already know what the related object is, seed the related
Expand Down Expand Up @@ -1601,7 +1595,6 @@ class ForeignObject(RelatedField):
one_to_many = False
one_to_one = False

allow_unsaved_instance_assignment = False
requires_unique_target = True
related_accessor_class = ForeignRelatedObjectsDescriptor
rel_class = ForeignObjectRel
Expand Down
7 changes: 0 additions & 7 deletions docs/ref/contrib/contenttypes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -299,13 +299,6 @@ model:
is ``True``. This mirrors the ``for_concrete_model`` argument to
: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

The "object_id" field doesn't have to be the same type as the
Expand Down
30 changes: 0 additions & 30 deletions docs/ref/models/fields.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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``.

.. 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:

``ManyToManyField``
Expand Down Expand Up @@ -1607,12 +1583,6 @@ that control how the relationship functions.

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`.

:attr:`~Field.null` has no effect since there is no way to require a
Expand Down
4 changes: 4 additions & 0 deletions docs/releases/1.8.4.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ Bugfixes

* Fixed the recording of squashed migrations when running the ``migrate``
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`).
18 changes: 16 additions & 2 deletions docs/releases/1.8.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
exception in a signal handler will prevent the whole operation.

.. _unsaved-model-instance-check-18:

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`,
:class:`~django.contrib.contenttypes.fields.GenericForeignKey`, and
:class:`~django.db.models.OneToOneField` now raises a :exc:`ValueError`.
Expand All @@ -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)
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
:attr:`~django.db.models.ForeignKey.allow_unsaved_instance_assignment`
attribute.
``ForeignKey.allow_unsaved_instance_assignment`` attribute. (This attribute was
removed in 1.8.4 as it's no longer relevant.)

Management commands that only accept positional arguments
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
16 changes: 7 additions & 9 deletions docs/topics/db/examples/many_to_one.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,17 @@ relationship. For example, creating an ``Article`` with unsaved ``Reporter``
raises ``ValueError``::

>>> 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):
...
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
:attr:`~django.db.models.ForeignKey.allow_unsaved_instance_assignment`
attribute.
.. versionchanged:: 1.8.4

.. versionchanged:: 1.8

Previously, assigning unsaved objects did not raise an error and could
result in silent data loss.
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
instances couldn't be assigned to related fields, but this restriction was
removed to allow easier usage of in-memory models.

Article objects have access to their related Reporter objects::

Expand Down
20 changes: 7 additions & 13 deletions docs/topics/db/examples/one_to_one.txt
Original file line number Diff line number Diff line change
Expand Up @@ -96,23 +96,17 @@ relationship. For example, creating an ``Restaurant`` with unsaved ``Place``
raises ``ValueError``::

>>> 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):
...
ValueError: 'Cannot assign "<Place: Demon Dogs>": "Place" instance isn't saved in the database.'
>>> 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
:attr:`~django.db.models.ForeignKey.allow_unsaved_instance_assignment`
attribute.
ValueError: save() prohibited to prevent data loss due to unsaved related object 'place'.

.. versionchanged:: 1.8
.. versionchanged:: 1.8.4

Previously, assigning unsaved objects did not raise an error and could
result in silent data loss.
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
instances couldn't be assigned to related fields, but this restriction was
removed to allow easier usage of in-memory models.

Restaurant.objects.all() just returns the Restaurants, not the Places. Note
that there are two restaurants - Ace Hardware the Restaurant was created in the
Expand Down
6 changes: 3 additions & 3 deletions tests/admin_utils/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
label_for_field, lookup_field, quote,
)
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.formats import localize
from django.utils.safestring import mark_safe
Expand Down Expand Up @@ -94,7 +94,7 @@ def test_relation_on_abstract(self):
n.collect([Vehicle.objects.first()])


class UtilsTests(TestCase):
class UtilsTests(SimpleTestCase):

empty_value = '-empty-'

Expand All @@ -115,7 +115,7 @@ def get_admin_value(self, obj):

simple_function = lambda obj: SIMPLE_FUNCTION

site_obj = Site.objects.create(domain=SITE_NAME)
site_obj = Site(domain=SITE_NAME)
article = Article(
site=site_obj,
title=TITLE_TEXT,
Expand Down
48 changes: 0 additions & 48 deletions tests/contenttypes_tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,54 +236,6 @@ class Model(models.Model):
errors = checks.run_checks()
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):

Expand Down
10 changes: 10 additions & 0 deletions tests/generic_relations/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.contrib.contenttypes.forms import generic_inlineformset_factory
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldError
from django.db import IntegrityError
from django.db.models import Q
from django.test import SimpleTestCase, TestCase
from django.utils import six
Expand Down Expand Up @@ -486,6 +487,15 @@ def test_query_content_type(self):
with self.assertRaisesMessage(FieldError, msg):
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):
pass
Expand Down
Loading

0 comments on commit 5980b05

Please sign in to comment.