Commit 5643a3b5 authored by Anubhav Joshi's avatar Anubhav Joshi Committed by Tim Graham
Browse files

Fixed #10811 -- Made assigning unsaved objects to FK, O2O, and GFK raise ValueError.

This prevents silent data loss.

Thanks Aymeric Augustin for the initial patch and Loic Bistuer for the review.
parent 4f72e5f0
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -223,6 +223,11 @@ class GenericForeignKey(object):
        if value is not None:
            ct = self.get_content_type(obj=value)
            fk = value._get_pk_val()
            if 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)
+12 −5
Original line number Diff line number Diff line
@@ -617,13 +617,20 @@ class ReverseSingleRelatedObjectDescriptor(object):
            if related is not None:
                setattr(related, self.field.related.get_cache_name(), None)

        # Set the value of the related field
            for lh_field, rh_field in self.field.related_fields:
            try:
                setattr(instance, lh_field.attname, getattr(value, rh_field.attname))
            except AttributeError:
                setattr(instance, lh_field.attname, None)

        # Set the values of the related field.
        else:
            for lh_field, rh_field in self.field.related_fields:
                val = getattr(value, rh_field.attname)
                if val is None:
                    raise ValueError(
                        'Cannot assign "%r": "%s" instance isn\'t saved in the database.' %
                        (value, self.field.rel.to._meta.object_name)
                    )
                setattr(instance, lh_field.attname, val)

        # Since we already know what the related object is, seed the related
        # object caches now, too. This avoids another db hit if you get the
        # object you just set.
+41 −13
Original line number Diff line number Diff line
@@ -218,19 +218,47 @@ Backwards incompatible changes in 1.8
    deprecation timeline for a given feature, its removal may appear as a
    backwards incompatible change.

* Some operations on related objects such as
Related object operations are run in a transaction
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Some operations on related objects such as
:meth:`~django.db.models.fields.related.RelatedManager.add()` or
:ref:`direct assignment<direct-assignment>` ran multiple data modifying
queries without wrapping them in transactions. To reduce the risk of data
corruption, all data modifying methods that affect multiple related objects
  (i.e. ``add()``, ``remove()``, ``clear()``, and
  :ref:`direct assignment<direct-assignment>`) now perform their data modifying
  queries from within a transaction, provided your database supports
  transactions.

  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.
(i.e. ``add()``, ``remove()``, ``clear()``, and :ref:`direct assignment
<direct-assignment>`) now perform their data modifying queries from within a
transaction, provided your database supports transactions.

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.

Unassigning unsaved objects to relations raises an error
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

Previously, the assignment of an unsaved object would be silently ignored.
For example::

    >>> book = Book.objects.create(name="Django")
    >>> book.author = Author(name="John")
    >>> book.author.save()
    >>> book.save()

    >>> Book.objects.get(name="Django")
    >>> book.author
    >>>

Now, an error will be raised to prevent data loss::

    >>> book.author = Author(name="john")
    Traceback (most recent call last):
    ...
    ValueError: Cannot assign "<Author: John>": "Author" instance isn't saved in the database.

Miscellaneous
~~~~~~~~~~~~~
+15 −0
Original line number Diff line number Diff line
@@ -52,6 +52,21 @@ Create an Article::
    >>> a.reporter
    <Reporter: John Smith>

Note that you must save an object before it can be assigned to a foreign key
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)
    Traceback (most recent call last):
    ...
    ValueError: 'Cannot assign "<Reporter: John Smith>": "Reporter" instance isn't saved in the database.'

.. versionchanged:: 1.8

    Previously, assigning unsaved objects did not raise an error and could
    result in silent data loss.

Article objects have access to their related Reporter objects::

    >>> r = a.reporter
+19 −0
Original line number Diff line number Diff line
@@ -89,6 +89,25 @@ Set the place back again, using assignment in the reverse direction::
    >>> p1.restaurant
    <Restaurant: Demon Dogs the restaurant>

Note that you must save an object before it can be assigned to a one-to-one
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)
    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.'

.. versionchanged:: 1.8

    Previously, assigning unsaved objects did not raise an error and could
    result in silent data loss.

Restaurant.objects.all() just returns the Restaurants, not the Places.  Note
that there are two restaurants - Ace Hardware the Restaurant was created in the
call to r.place = p2::
Loading