Ticket #7539: on_delete_on_update-r11733.diff

File on_delete_on_update-r11733.diff, 45.7 KB (added by glassfordm, 14 years ago)

Same as my previous patch, but including unintentionally omitted unit tests

  • django/db/models/base.py

     
    88from django.db.models.fields import AutoField, FieldDoesNotExist
    99from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
    1010from django.db.models.query import delete_objects, Q
    11 from django.db.models.query_utils import CollectedObjects, DeferredAttribute
     11from django.db.models.query_utils import CollectedFields, CollectedObjects, DeferredAttribute
    1212from django.db.models.options import Options
    13 from django.db import connection, transaction, DatabaseError
     13from django.db import connection, transaction, DatabaseError, IntegrityError
    1414from django.db.models import signals
     15from django.db.models.fields.related import CASCADE, PROTECT, SET_NULL, SET_DEFAULT
    1516from django.db.models.loading import register_models, get_model
    1617from django.utils.functional import curry
    1718from django.utils.encoding import smart_str, force_unicode, smart_unicode
     
    516517
    517518    save_base.alters_data = True
    518519
    519     def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):
     520    def _collect_sub_objects(self, seen_objs, fields_to_set, parent=None, nullable=False):
    520521        """
    521522        Recursively populates seen_objs with all objects related to this
    522523        object.
     
    528529        pk_val = self._get_pk_val()
    529530        if seen_objs.add(self.__class__, pk_val, self, parent, nullable):
    530531            return
     532           
     533        def _handle_sub_obj(related, sub_obj):
     534            on_delete = related.field.rel.on_delete
     535            if on_delete is None:
     536                #If no explicit on_delete option is specified, use the old
     537                #django behavior as the default: SET_NULL if the foreign
     538                #key is nullable, otherwise CASCADE.
     539                if related.field.null:
     540                    on_delete = SET_NULL
     541                else:
     542                    on_delete = CASCADE
     543               
     544            if on_delete == CASCADE:
     545                sub_obj._collect_sub_objects(seen_objs, fields_to_set, self.__class__)
     546            elif on_delete == PROTECT:
     547                msg = '[Django] Cannot delete a parent object: a foreign key constraint fails (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % (
     548                    sub_obj.__class__,
     549                    sub_obj._get_pk_val(),
     550                    self.__class__,
     551                    pk_val,
     552                    )
     553                raise IntegrityError(msg)
     554            elif on_delete == SET_NULL:
     555                if not related.field.null:
     556                    msg = '[Django] Cannot delete a parent object: foreign key constraint on_delete=SET_NULL is specified for a non-nullable foreign key (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % (
     557                        sub_obj.__class__,
     558                        sub_obj._get_pk_val(),
     559                        self.__class__,
     560                        pk_val,
     561                        )
     562                    raise IntegrityError(msg)
     563                fields_to_set.add(sub_obj.__class__, sub_obj._get_pk_val(), sub_obj, related.field.name, None)
     564            elif on_delete == SET_DEFAULT:
     565                if not related.field.has_default():
     566                    msg = '[Django] Cannot delete a parent object: foreign key constraint on_delete=SET_DEFAULT is specified for a foreign key with no default value (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % (
     567                        sub_obj.__class__,
     568                        sub_obj._get_pk_val(),
     569                        self.__class__,
     570                        pk_val,
     571                        )
     572                    raise IntegrityError(msg)
     573                fields_to_set.add(sub_obj.__class__, sub_obj._get_pk_val(), sub_obj, related.field.name, related.field.get_default())
     574            else:
     575                raise AttributeError('Unexpected value for on_delete')
    531576
    532577        for related in self._meta.get_all_related_objects():
    533578            rel_opts_name = related.get_accessor_name()
    534579            if isinstance(related.field.rel, OneToOneRel):
    535580                try:
     581                    # delattr(self, rel_opts_name) #Delete first to clear any stale cache
     582                        #TODO: the above line is a bit of a hack
     583                        #It's one way (not a very good one) to work around stale cache data causing
     584                        #spurious RESTRICT errors, etc; it would be better to prevent the cache from
     585                        #becoming stale in the first place.
    536586                    sub_obj = getattr(self, rel_opts_name)
    537587                except ObjectDoesNotExist:
    538588                    pass
    539589                else:
    540                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
     590                    _handle_sub_obj(related, sub_obj)
    541591            else:
    542592                # To make sure we can access all elements, we can't use the
    543593                # normal manager on the related object. So we work directly
     
    555605                        continue
    556606                delete_qs = rel_descriptor.delete_manager(self).all()
    557607                for sub_obj in delete_qs:
    558                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
     608                    _handle_sub_obj(related, sub_obj)
    559609
    560610        # Handle any ancestors (for the model-inheritance case). We do this by
    561611        # traversing to the most remote parent classes -- those with no parents
     
    570620                continue
    571621            # At this point, parent_obj is base class (no ancestor models). So
    572622            # delete it and all its descendents.
    573             parent_obj._collect_sub_objects(seen_objs)
     623            parent_obj._collect_sub_objects(seen_objs, fields_to_set)
    574624
    575625    def delete(self):
    576626        assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname)
    577627
    578628        # Find all the objects than need to be deleted.
    579629        seen_objs = CollectedObjects()
    580         self._collect_sub_objects(seen_objs)
     630        fields_to_set = CollectedFields()
     631        self._collect_sub_objects(seen_objs, fields_to_set)
    581632
    582633        # Actually delete the objects.
    583         delete_objects(seen_objs)
    584 
     634        delete_objects(seen_objs, fields_to_set)
    585635    delete.alters_data = True
    586636
    587637    def _get_FIELD_display(self, field):
  • django/db/models/fields/related.py

     
    2020
    2121pending_lookups = {}
    2222
     23class CASCADE(object):
     24    pass
     25class PROTECT(object):
     26    pass
     27class SET_NULL(object):
     28    pass
     29class SET_DEFAULT(object):
     30    pass
     31ALLOWED_ON_DELETE_ACTION_TYPES = set([None, CASCADE, PROTECT, SET_NULL, SET_DEFAULT])
     32
    2333def add_lazy_relation(cls, field, relation, operation):
    2434    """
    2535    Adds a lookup on ``cls`` when a related field is defined using a string,
     
    222232        # object you just set.
    223233        setattr(instance, self.cache_name, value)
    224234        setattr(value, self.related.field.get_cache_name(), instance)
     235       
     236    #TODO: the following function is a bit of a hack
     237        #It's one way (not a very good one) to work around stale cache data causing
     238        #spurious RESTRICT errors, etc; it would be better to prevent the cache from
     239        #becoming stale in the first place.
     240    # def __delete__(self, instance):
     241    #     try:
     242    #         return delattr(instance, self.cache_name)
     243    #     except AttributeError:
     244    #         pass
    225245
    226246class ReverseSingleRelatedObjectDescriptor(object):
    227247    # This class provides the functionality that makes the related-object
     
    625645
    626646class ManyToOneRel(object):
    627647    def __init__(self, to, field_name, related_name=None,
    628             limit_choices_to=None, lookup_overrides=None, parent_link=False):
     648            limit_choices_to=None, lookup_overrides=None, parent_link=False,
     649            on_delete=None):
    629650        try:
    630651            to._meta
    631652        except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
     
    638659        self.lookup_overrides = lookup_overrides or {}
    639660        self.multiple = True
    640661        self.parent_link = parent_link
     662        self.on_delete = on_delete
    641663
    642664    def is_hidden(self):
    643665        "Should the related object be hidden?"
     
    656678
    657679class OneToOneRel(ManyToOneRel):
    658680    def __init__(self, to, field_name, related_name=None,
    659             limit_choices_to=None, lookup_overrides=None, parent_link=False):
     681            limit_choices_to=None, lookup_overrides=None, parent_link=False,
     682            on_delete=None):
    660683        super(OneToOneRel, self).__init__(to, field_name,
    661684                related_name=related_name, limit_choices_to=limit_choices_to,
    662                 lookup_overrides=lookup_overrides, parent_link=parent_link)
     685                lookup_overrides=lookup_overrides, parent_link=parent_link,
     686                on_delete=on_delete)
    663687        self.multiple = False
    664688
    665689class ManyToManyRel(object):
     
    705729            related_name=kwargs.pop('related_name', None),
    706730            limit_choices_to=kwargs.pop('limit_choices_to', None),
    707731            lookup_overrides=kwargs.pop('lookup_overrides', None),
    708             parent_link=kwargs.pop('parent_link', False))
     732            parent_link=kwargs.pop('parent_link', False),
     733            on_delete=kwargs.pop('on_delete', None))
    709734        Field.__init__(self, **kwargs)
    710735
    711736        self.db_index = True
     
    750775            target = self.rel.to._meta.db_table
    751776        cls._meta.duplicate_targets[self.column] = (target, "o2m")
    752777
     778        on_delete = self.rel.on_delete
     779        if on_delete not in ALLOWED_ON_DELETE_ACTION_TYPES:
     780            raise ValueError("Invalid value 'on_delete=%s' specified for %s %s.%s." % (on_delete, type(self).__name__, cls.__name__, name))
     781        if on_delete == SET_NULL and not self.null:
     782            specification = "'on_delete=SET_NULL'"
     783            raise ValueError("%s specified for %s '%s.%s', but the field is not nullable." % (specification, type(self).__name__, cls.__name__, name))
     784        if on_delete == SET_DEFAULT and not self.has_default():
     785            specification = "'on_delete=SET_DEFAULT'"
     786            raise ValueError("%s specified for %s '%s.%s', but the field has no default value." % (specification, type(self).__name__, cls.__name__, name))
     787
    753788    def contribute_to_related_class(self, cls, related):
    754789        # Internal FK's - i.e., those with a related name ending with '+' -
    755790        # don't get a related descriptor.
  • django/db/models/__init__.py

     
    1111from django.db.models.fields.subclassing import SubfieldBase
    1212from django.db.models.fields.files import FileField, ImageField
    1313from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel
     14from django.db.models.fields.related import CASCADE, PROTECT, SET_NULL, SET_DEFAULT
    1415from django.db.models import signals
    1516
    1617# Admin stages.
  • django/db/models/query.py

     
    55from copy import deepcopy
    66from django.db import connection, transaction, IntegrityError
    77from django.db.models.aggregates import Aggregate
     8from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE
    89from django.db.models.fields import DateField
    9 from django.db.models.query_utils import Q, select_related_descend, CollectedObjects, CyclicDependency, deferred_class_factory
     10from django.db.models.query_utils import Q, select_related_descend, CollectedFields, CollectedObjects, CyclicDependency, deferred_class_factory
    1011from django.db.models import signals, sql
    1112
    1213# Used to control how many objects are worked with at once in some cases (e.g.
     
    384385            # Collect all the objects to be deleted in this chunk, and all the
    385386            # objects that are related to the objects that are to be deleted.
    386387            seen_objs = CollectedObjects(seen_objs)
     388            fields_to_set = CollectedFields()
    387389            for object in del_query[:CHUNK_SIZE]:
    388                 object._collect_sub_objects(seen_objs)
     390                object._collect_sub_objects(seen_objs, fields_to_set)
    389391
    390392            if not seen_objs:
    391393                break
    392             delete_objects(seen_objs)
     394            delete_objects(seen_objs, fields_to_set)
    393395
    394396        # Clear the result cache, in case this QuerySet gets reused.
    395397        self._result_cache = None
     
    10001002                setattr(obj, f.get_cache_name(), rel_obj)
    10011003    return obj, index_end
    10021004
    1003 def delete_objects(seen_objs):
     1005def delete_objects(seen_objs, fields_to_set):
    10041006    """
    10051007    Iterate through a list of seen classes, and remove any instances that are
    10061008    referred to.
     
    10211023
    10221024    obj_pairs = {}
    10231025    try:
     1026        for cls, cls_dct in fields_to_set.iteritems():
     1027            #TODO: batch these, similar to UpdateQuery.clear_related?
     1028            #(Note that it may be harder to do here because the default value
     1029            #for a given field may be different for each instance,
     1030            #while UpdateQuery.clear_related always uses the value None).
     1031            query = sql.UpdateQuery(cls, connection)
     1032            for instance, field_names_and_values in cls_dct.itervalues():
     1033                query.where = query.where_class()
     1034                pk = query.model._meta.pk
     1035                query.where.add((sql.where.Constraint(None, pk.column, pk), 'exact', instance.pk), sql.where.AND)
     1036                query.add_update_values(field_names_and_values)
     1037                query.execute_sql()
     1038                   
    10241039        for cls in ordered_classes:
    10251040            items = seen_objs[cls].items()
    10261041            items.sort()
     
    10311046                if not cls._meta.auto_created:
    10321047                    signals.pre_delete.send(sender=cls, instance=instance)
    10331048
     1049            # Handle related GenericRelation and ManyToManyField instances
    10341050            pk_list = [pk for pk,instance in items]
    10351051            del_query = sql.DeleteQuery(cls, connection)
    10361052            del_query.delete_batch_related(pk_list)
    10371053
    1038             update_query = sql.UpdateQuery(cls, connection)
    1039             for field, model in cls._meta.get_fields_with_model():
    1040                 if (field.rel and field.null and field.rel.to in seen_objs and
    1041                         filter(lambda f: f.column == field.rel.get_related_field().column,
    1042                         field.rel.to._meta.fields)):
    1043                     if model:
    1044                         sql.UpdateQuery(model, connection).clear_related(field,
    1045                                 pk_list)
    1046                     else:
    1047                         update_query.clear_related(field, pk_list)
    1048 
    1049         # Now delete the actual data.
    10501054        for cls in ordered_classes:
    10511055            items = obj_pairs[cls]
    10521056            items.reverse()
    1053 
    10541057            pk_list = [pk for pk,instance in items]
    10551058            del_query = sql.DeleteQuery(cls, connection)
    10561059            del_query.delete_batch(pk_list)
    10571060
    1058             # Last cleanup; set NULLs where there once was a reference to the
    1059             # object, NULL the primary key of the found objects, and perform
    1060             # post-notification.
     1061        #Last cleanup; set NULLs and default values where there once was a
     1062        #reference to the object, NULL the primary key of the found objects,
     1063        #and perform post-notification.
     1064        for cls, cls_dct in fields_to_set.iteritems():
     1065            for instance, field_names_and_values in cls_dct.itervalues():
     1066                for field_name, field_value in field_names_and_values.iteritems():
     1067                    field = cls._meta.get_field_by_name(field_name)[0]
     1068                    setattr(instance, field.attname, field_value)
     1069        for cls in ordered_classes:
     1070            items = obj_pairs[cls]
     1071            items.reverse()
    10611072            for pk_val, instance in items:
    10621073                for field in cls._meta.fields:
    10631074                    if field.rel and field.null and field.rel.to in seen_objs:
  • django/db/models/query_utils.py

     
    124124        """
    125125        return self.data.keys()
    126126
     127class CollectedFields(object):
     128    """
     129    A container that stores the model object and field name
     130    for fields that need to  be set to enforce on_delete=SET_NULL
     131    and on_delete=SET_DEFAULT ForeigKey constraints.
     132    """
     133
     134    def __init__(self):
     135        self.data = {}
     136
     137    def add(self, model, pk, obj, field_name, field_value):
     138        """
     139        Adds an item.
     140        model is the class of the object being added,
     141        pk is the primary key, obj is the object itself,
     142        field_name is the name of the field to be set,
     143        field_value is the value it needs to be set to.
     144        """
     145        d = self.data.setdefault(model, SortedDict())
     146        obj, field_names_and_values = d.setdefault(pk, (obj, dict()))
     147        assert field_name not in field_names_and_values or field_names_and_values[field_name] == field_value
     148        field_names_and_values[field_name] = field_value
     149
     150    def __contains__(self, key):
     151        return self.data.__contains__(key)
     152
     153    def __getitem__(self, key):
     154        return self.data[key]
     155
     156    def __nonzero__(self):
     157        return bool(self.data)
     158
     159    def iteritems(self):
     160        return self.data.iteritems()
     161
     162    def iterkeys(self):
     163        return self.data.iterkeys()
     164
     165    def itervalues(self):
     166        return self.data.itervalues()
     167
     168    def items(self):
     169        return self.data.items()
     170
     171    def keys(self):
     172        return self.data.keys()
     173
     174    def values(self):
     175        return self.data.values()
     176
    127177class QueryWrapper(object):
    128178    """
    129179    A type that indicates the contents are an SQL fragment and the associate
  • tests/modeltests/on_delete_django/tests.py

    Property changes on: tests/modeltests/on_delete_django/__init__.py
    ___________________________________________________________________
    Name: svn:keywords
       + Id
    
     
     1from django.conf import settings
     2from django.db import IntegrityError, models
     3from django.db.models.fields.related import CASCADE, PROTECT, SET_NULL, SET_DEFAULT
     4from django.test import TestCase
     5
     6from models import *
     7
     8class ON_DELETE_Tests(TestCase):
     9    ### ForeignKey tests
     10
     11    def test_ForeignKey_CASCADE(self):
     12        self._CASCADE_test(ForeignKey_CASCADE, True)
     13
     14    def test_ForeignKey_PROTECT(self):
     15        self._PROTECT_test(ForeignKey_PROTECT, True)
     16
     17    def test_ForeignKey_SET_NULL(self):
     18        self._SET_NULL_test(ForeignKey_SET_NULL, True)
     19
     20    def test_ForeignKey_SET_DEFAULT(self):
     21        self._SET_DEFAULT_test(ForeignKey_SET_DEFAULT, True)
     22   
     23    def test_ForeignKey_None_Null_True(self):
     24        #If no on_delete behavior is specified,
     25        #Django treats it as SET_NULL if the field is nullable
     26        self._SET_NULL_test(ForeignKey_None_Null_True, True)
     27
     28    def test_ForeignKey_None_Null_False(self):
     29        #If no on_delete behavior is specified,
     30        #Django treats it as CASCADE if it is not nullable.
     31        self._CASCADE_test(ForeignKey_None_Null_False, True)
     32
     33    def test_ForeignKey_Combined(self):
     34        self._combined_test(ForeignKey_CASCADE, ForeignKey_PROTECT, ForeignKey_SET_NULL, ForeignKey_SET_DEFAULT)
     35   
     36    def test_ForeignKey_SET_NULL_requires_nullable_field(self):
     37        try:
     38            class Test(models.Model):
     39                fk = models.ForeignKey(Data, to_field='data', on_delete=models.SET_NULL)
     40        except ValueError, e:
     41            self.assertEqual(str(e), "'on_delete=SET_NULL' specified for ForeignKey 'Test.fk', but the field is not nullable.")
     42        else:
     43            self.assertTrue(False, 'Expected exception not raised.')
     44
     45    def test_ForeignKey_SET_DEFAULT_requires_default_value(self):
     46        try:
     47            class Test(models.Model):
     48                fk = models.ForeignKey(Data, to_field='data', on_delete=models.SET_DEFAULT)
     49        except ValueError, e:
     50            self.assertEqual(str(e), "'on_delete=SET_DEFAULT' specified for ForeignKey 'Test.fk', but the field has no default value.")
     51        else:
     52            self.assertTrue(False, 'Expected exception not raised.')
     53           
     54    ### OneToOneField tests
     55
     56    def test_OneToOne_CASCADE(self):
     57        self._CASCADE_test(OneToOne_CASCADE, False)
     58   
     59    def test_OneToOne_PROTECT(self):
     60        self._PROTECT_test(OneToOne_PROTECT, False)
     61   
     62    def test_OneToOne_SET_NULL(self):
     63        self._SET_NULL_test(OneToOne_SET_NULL, False)
     64   
     65    def test_OneToOne_SET_DEFAULT(self):
     66        self._SET_DEFAULT_test(OneToOne_SET_DEFAULT, False)
     67   
     68    def test_OneToOne_None_Null_True(self):
     69        #If no on_delete behavior is specified,
     70        #Django treats it as SET_NULL if the field is nullable
     71        self._SET_NULL_test(OneToOne_None_Null_True, False)
     72   
     73    def test_OneToOne_None_Null_False(self):
     74        #If no on_delete behavior is specified,
     75        #Django treats it as CASCADE if it is not nullable.
     76        self._CASCADE_test(OneToOne_None_Null_False, False)
     77   
     78    def test_OneToOne_Combined(self):
     79        self._combined_test(OneToOne_CASCADE, OneToOne_PROTECT, OneToOne_SET_NULL, OneToOne_SET_DEFAULT)
     80   
     81    def test_OneToOne_SET_NULL_requires_nullable_field(self):
     82        try:
     83            class Test(models.Model):
     84                fk = models.OneToOneField(Data, to_field='data', on_delete=models.SET_NULL)
     85        except ValueError, e:
     86            self.assertEqual(str(e), "'on_delete=SET_NULL' specified for OneToOneField 'Test.fk', but the field is not nullable.")
     87        else:
     88            self.assertTrue(False, 'Expected exception not raised.')
     89
     90    def test_OneToOne_SET_DEFAULT_requires_default_value(self):
     91        try:
     92            class Test(models.Model):
     93                fk = models.OneToOneField(Data, to_field='data', on_delete=models.SET_DEFAULT)
     94        except ValueError, e:
     95            self.assertEqual(str(e), "'on_delete=SET_DEFAULT' specified for OneToOneField 'Test.fk', but the field has no default value.")
     96        else:
     97            self.assertTrue(False, 'Expected exception not raised.')
     98           
     99    ### Multiple FK tests
     100   
     101    def test_All_FK_Model_1(self):
     102        data = Data.objects.create(data=1)
     103        fk = All_FK_Model.objects.create(
     104            fk_ForeignKey_Cascade=data,
     105            fk_ForeignKey_Protect=data,
     106            fk_ForeignKey_Set_Null=data,
     107            fk_ForeignKey_Set_Default=data,
     108            fk_OneToOneField_Cascade=data,
     109            fk_OneToOneField_Protect=data,
     110            fk_OneToOneField_Set_Null=data,
     111            fk_OneToOneField_Set_Default=data,
     112            )
     113        fk_id = fk.pk
     114        self.assertEqual(data, fk.fk_ForeignKey_Cascade)
     115        self.assertEqual(data, fk.fk_ForeignKey_Protect)
     116        self.assertEqual(data, fk.fk_ForeignKey_Set_Null)
     117        self.assertEqual(data, fk.fk_ForeignKey_Set_Default)
     118        self.assertEqual(data, fk.fk_OneToOneField_Cascade)
     119        self.assertEqual(data, fk.fk_OneToOneField_Protect)
     120        self.assertEqual(data, fk.fk_OneToOneField_Set_Null)
     121        self.assertEqual(data, fk.fk_OneToOneField_Set_Default)
     122        self.assertRaises(IntegrityError, data.delete)
     123       
     124        fk.fk_ForeignKey_Protect = None
     125        fk.fk_OneToOneField_Protect = data
     126        fk.save()
     127        self.assertRaises(IntegrityError, data.delete)
     128
     129        fk.fk_ForeignKey_Protect = data
     130        fk.fk_OneToOneField_Protect = None
     131        fk.save()
     132        self.assertRaises(IntegrityError, data.delete)
     133
     134        fk.fk_ForeignKey_Protect = None
     135        fk.fk_OneToOneField_Protect = None
     136        fk.save()
     137        data.delete()
     138        self.assertRaises(All_FK_Model.DoesNotExist, All_FK_Model.objects.get, id=fk_id)
     139
     140    def test_All_FK_Model_2(self):
     141        default_value = default_data()
     142
     143        data = Data.objects.create(data=1)
     144        fk = All_FK_Model.objects.create(
     145            fk_ForeignKey_Cascade=None,
     146            fk_ForeignKey_Protect=None,
     147            fk_ForeignKey_Set_Null=data,
     148            fk_ForeignKey_Set_Default=data,
     149            fk_OneToOneField_Cascade=None,
     150            fk_OneToOneField_Protect=None,
     151            fk_OneToOneField_Set_Null=data,
     152            fk_OneToOneField_Set_Default=data,
     153            )
     154        fk_id = fk.pk
     155        self.assertEqual(None, fk.fk_ForeignKey_Cascade)
     156        self.assertEqual(None, fk.fk_ForeignKey_Protect)
     157        self.assertEqual(data, fk.fk_ForeignKey_Set_Null)
     158        self.assertEqual(data, fk.fk_ForeignKey_Set_Default)
     159        self.assertEqual(None, fk.fk_OneToOneField_Cascade)
     160        self.assertEqual(None, fk.fk_OneToOneField_Protect)
     161        self.assertEqual(data, fk.fk_OneToOneField_Set_Null)
     162        self.assertEqual(data, fk.fk_OneToOneField_Set_Default)
     163
     164        data.delete()
     165       
     166        fk = All_FK_Model.objects.get(pk=fk_id) #TODO: fix bug that makes this necessary
     167        self.assertEqual(None, fk.fk_ForeignKey_Cascade)
     168        self.assertEqual(None, fk.fk_ForeignKey_Protect)
     169        self.assertEqual(None, fk.fk_ForeignKey_Set_Null)
     170        self.assertEqual(default_value, fk.fk_ForeignKey_Set_Default)
     171        self.assertEqual(None, fk.fk_OneToOneField_Cascade)
     172        self.assertEqual(None, fk.fk_OneToOneField_Protect)
     173        self.assertEqual(None, fk.fk_OneToOneField_Set_Null)
     174        self.assertEqual(default_value, fk.fk_OneToOneField_Set_Default)
     175
     176    #TODO: not done yet:
     177    # def test_All_FK_Model_3(self):
     178    #     default_value = default_data()
     179    #
     180    #     data = Data.objects.create(data=1)
     181    #     fk1 = All_FK_Model.objects.create(
     182    #         fk_ForeignKey_Cascade=None,
     183    #         fk_ForeignKey_Protect=None,
     184    #         fk_ForeignKey_Set_Null=data,
     185    #         fk_ForeignKey_Set_Default=data,
     186    #         fk_OneToOneField_Cascade=None,
     187    #         fk_OneToOneField_Protect=None,
     188    #         fk_OneToOneField_Set_Null=data,
     189    #         fk_OneToOneField_Set_Default=data,
     190    #         )
     191    #     fk2 = All_FK_Model.objects.create(
     192    #         fk_ForeignKey_Cascade=None,
     193    #         fk_ForeignKey_Protect=None,
     194    #         fk_ForeignKey_Set_Null=data,
     195    #         fk_ForeignKey_Set_Default=data,
     196    #         fk_OneToOneField_Cascade=None,
     197    #         fk_OneToOneField_Protect=None,
     198    #         fk_OneToOneField_Set_Null=None,
     199    #         fk_OneToOneField_Set_Default=None,
     200    #         )
     201    #     fk1_id = fk1.pk
     202    #     fk2_id = fk2.pk
     203    #     self.assertEqual(None, fk1.fk_ForeignKey_Cascade)
     204    #     self.assertEqual(None, fk1.fk_ForeignKey_Protect)
     205    #     self.assertEqual(data, fk1.fk_ForeignKey_Set_Null)
     206    #     self.assertEqual(data, fk1.fk_ForeignKey_Set_Default)
     207    #     self.assertEqual(None, fk1.fk_OneToOneField_Cascade)
     208    #     self.assertEqual(None, fk1.fk_OneToOneField_Protect)
     209    #     self.assertEqual(data, fk1.fk_OneToOneField_Set_Null)
     210    #     self.assertEqual(data, fk1.fk_OneToOneField_Set_Default)
     211    #     self.assertEqual(None, fk2.fk_ForeignKey_Cascade)
     212    #     self.assertEqual(None, fk2.fk_ForeignKey_Protect)
     213    #     self.assertEqual(data, fk2.fk_ForeignKey_Set_Null)
     214    #     self.assertEqual(data, fk2.fk_ForeignKey_Set_Default)
     215    #     self.assertEqual(None, fk2.fk_OneToOneField_Cascade)
     216    #     self.assertEqual(None, fk2.fk_OneToOneField_Protect)
     217    #     self.assertEqual(None, fk2.fk_OneToOneField_Set_Null)
     218    #     self.assertEqual(None, fk2.fk_OneToOneField_Set_Default)
     219    #
     220    #     data.delete()
     221    #     
     222    #     fk1 = All_FK_Model.objects.get(pk=fk1_id) #TODO: fix bug that makes this necessary
     223    #     fk2 = All_FK_Model.objects.get(pk=fk2_id) #TODO: fix bug that makes this necessary
     224    #     self.assertEqual(None, fk1.fk_ForeignKey_Cascade)
     225    #     self.assertEqual(None, fk1.fk_ForeignKey_Protect)
     226    #     self.assertEqual(None, fk1.fk_ForeignKey_Set_Null)
     227    #     self.assertEqual(default_value, fk1.fk_ForeignKey_Set_Default)
     228    #     self.assertEqual(None, fk1.fk_OneToOneField_Cascade)
     229    #     self.assertEqual(None, fk1.fk_OneToOneField_Protect)
     230    #     self.assertEqual(None, fk1.fk_OneToOneField_Set_Null)
     231    #     self.assertEqual(default_value, fk1.fk_OneToOneField_Set_Default)
     232    #     self.assertEqual(None, fk2.fk_ForeignKey_Cascade)
     233    #     self.assertEqual(None, fk2.fk_ForeignKey_Protect)
     234    #     self.assertEqual(None, fk2.fk_ForeignKey_Set_Null)
     235    #     self.assertEqual(default_value, fk2.fk_ForeignKey_Set_Default)
     236    #     self.assertEqual(None, fk2.fk_OneToOneField_Cascade)
     237    #     self.assertEqual(None, fk2.fk_OneToOneField_Protect)
     238    #     self.assertEqual(None, fk2.fk_OneToOneField_Set_Null)
     239    #     self.assertEqual(default_value, fk2.fk_OneToOneField_Set_Default)
     240       
     241    ### Cache tests
     242   
     243    #These tests illustrate a caching problem with OneToOneKeyField.
     244    def test_issue_1a(self):
     245        data = Data.objects.create(data=1)
     246        fk = OneToOne_PROTECT.objects.create(fk=data)
     247        fk.delete()
     248        data.delete() #This succeeds
     249    def test_issue_1b(self):
     250        data = Data.objects.create(data=1)
     251        fk = OneToOne_PROTECT.objects.create(fk=data)
     252        self.assertRaises(IntegrityError, data.delete) #This line added
     253            #This loads the item into the SingleRelatedObjectDescriptor cache;
     254            #this cached item is not unloaded after the fk.delete(),
     255            #which later causes the data.delete() to fail.
     256        fk.delete()
     257        data.delete() #This fails
     258    def test_issue_1c(self):
     259        data = Data.objects.create(data=1)
     260        fk = ForeignKey_PROTECT.objects.create(fk=data) #This line changed
     261            #This problem applies only to OneToOneField;
     262            #ForeignKey doesn't have the same problem
     263        self.assertRaises(IntegrityError, data.delete)
     264        fk.delete()
     265        data.delete() #This succeeds
     266    def test_issue_1d(self):
     267        data = Data.objects.create(data=1)
     268        fk = OneToOne_PROTECT.objects.create(fk=data)
     269        self.assertRaises(IntegrityError, data.delete)
     270        fk = OneToOne_PROTECT.objects.get(pk=fk.pk)
     271        fk.delete()
     272        data.delete() #This fails
     273    def test_issue_1e(self):
     274        data = Data.objects.create(data=1)
     275        fk = OneToOne_PROTECT.objects.create(fk=data)
     276        self.assertRaises(IntegrityError, data.delete)
     277        fk.delete()
     278        data = Data.objects.get(pk=data.pk)
     279        data.delete() #This succeeds
     280            #Refreshing the data object unloads/refreshes its cache
     281
     282    #These tests illustrate a problem with both ForeignKey and OneToOneKeyField.
     283    #The database is changed correctly, but instances in memory are not updated correctly.
     284    #The problem is that, although some instances are changed in memory, other instances
     285    #referring to the same database record are not, and these become out of sync
     286    #with the db data. If these out of sync instances are then used, issues occur.
     287    def test_issue_2a(self):
     288        default_value = default_data()
     289        data = Data.objects.create(data=1)
     290        fk = ForeignKey_SET_DEFAULT.objects.create(fk=data)
     291        data.delete()       
     292        self.assertEqual(default_value, fk.fk) #Fails
     293    def test_issue_2b(self):
     294        data = Data.objects.create(data=1)
     295        fk = ForeignKey_SET_NULL.objects.create(fk=data)
     296        data.delete()       
     297        self.assertEqual(None, fk.fk) #Fails
     298    def test_issue_2c(self):
     299        data = Data.objects.create(data=1)
     300        fk = ForeignKey_SET_NULL.objects.create(fk=data)
     301        data.delete()       
     302        fk = ForeignKey_SET_NULL.objects.get(pk=fk.pk) #Added this line
     303        self.assertEqual(None, fk.fk) #Succeeds
     304    def test_issue_2d(self):
     305        default_value = default_data()
     306        data = Data.objects.create(data=1)
     307        fk = OneToOne_SET_DEFAULT.objects.create(fk=data)
     308        data.delete()       
     309        self.assertEqual(default_value, fk.fk) #Fails
     310    def test_issue_2e(self):
     311        data = Data.objects.create(data=1)
     312        fk = OneToOne_SET_NULL.objects.create(fk=data)
     313        data.delete()       
     314        self.assertEqual(None, fk.fk) #Fails
     315    def test_issue_2f(self):
     316        data = Data.objects.create(data=1)
     317        fk = OneToOne_SET_NULL.objects.create(fk=data)
     318        data.delete()       
     319        fk = OneToOne_SET_NULL.objects.get(pk=fk.pk) #Added this line
     320        self.assertEqual(None, fk.fk) #Succeeds
     321
     322    ### Utility methods
     323   
     324    def _CASCADE_test(self, foreign_key_model, many_to_one):
     325        #Create a data item and foreign key(s) to it
     326        data, fk_id_1 = self._create_data_item_and_foreign_key(1, foreign_key_model)
     327        if many_to_one:
     328            _, fk_id_2 = self._create_data_item_and_foreign_key(1, foreign_key_model)
     329
     330        #Create an unrelated data item and foreign key
     331        _, fk_id_unlrelated = self._create_data_item_and_foreign_key(2, foreign_key_model)
     332
     333        #Delete the data item and make sure the foreign key is affected appropriately
     334        data.delete()
     335        self.assertEqual(None, data.pk)
     336        self.assertRaises(foreign_key_model.DoesNotExist, foreign_key_model.objects.get, id=fk_id_1)
     337        if many_to_one:
     338            self.assertRaises(foreign_key_model.DoesNotExist, foreign_key_model.objects.get, id=fk_id_2)
     339   
     340        #Make sure the unrelated data item and foreign key are unaffected
     341        self._test_data_item_and_foreign_key(None, 2, foreign_key_model, fk_id_unlrelated)
     342
     343    def _PROTECT_test(self, foreign_key_model, many_to_one):
     344        #Create a data item and foreign key(s) to it
     345        data, fk_id_1 = self._create_data_item_and_foreign_key(1, foreign_key_model)
     346        if many_to_one:
     347            _, fk_id_2 = self._create_data_item_and_foreign_key(1, foreign_key_model)
     348
     349        #Create an unrelated data item and foreign key
     350        data_unrelated, fk_id_unlrelated = self._create_data_item_and_foreign_key(2, foreign_key_model)
     351
     352        #Delete the data item and make sure the foreign key is affected appropriately
     353        self.assertRaises(IntegrityError, data.delete)
     354        self.assertNotEqual(None, data.pk)
     355        self._test_data_item_and_foreign_key(data, 1, foreign_key_model, fk_id_1)
     356        if many_to_one:
     357            self._test_data_item_and_foreign_key(None, 1, foreign_key_model, fk_id_2)
     358
     359        #Make sure the unrelated data item and foreign key are unaffected
     360        self._test_data_item_and_foreign_key(data_unrelated, 2, foreign_key_model, fk_id_unlrelated)
     361
     362    def _SET_NULL_test(self, foreign_key_model, many_to_one):
     363        #Create a data item and foreign key(s) to it
     364        data, fk_id_1 = self._create_data_item_and_foreign_key(1, foreign_key_model)
     365        if many_to_one:
     366            _, fk_id_2 = self._create_data_item_and_foreign_key(1, foreign_key_model)
     367
     368        #Create an unrelated data item and foreign key
     369        data_unrelated, fk_id_unlrelated = self._create_data_item_and_foreign_key(2, foreign_key_model)
     370
     371        #Delete the data item and make sure the foreign key is affected appropriately
     372        data.delete()
     373        self.assertEqual(None, data.pk)
     374        self.assertEqual(None, foreign_key_model.objects.get(id=fk_id_1).fk)
     375        self.assertEqual(None, foreign_key_model.objects.get(id=fk_id_1).fk_id)
     376        if many_to_one:
     377            self.assertEqual(None, foreign_key_model.objects.get(id=fk_id_2).fk)
     378            self.assertEqual(None, foreign_key_model.objects.get(id=fk_id_2).fk_id)
     379
     380        #Make sure the unrelated data item and foreign key are unaffected
     381        self._test_data_item_and_foreign_key(data_unrelated, 2, foreign_key_model, fk_id_unlrelated)
     382
     383    def _SET_DEFAULT_test(self, foreign_key_model, many_to_one):
     384        #Create a data item and foreign key(s) to it
     385        data, fk_id_1 = self._create_data_item_and_foreign_key(1, foreign_key_model)
     386        if many_to_one:
     387            _, fk_id_2 = self._create_data_item_and_foreign_key(1, foreign_key_model)
     388
     389        #Create an unrelated data item and foreign key
     390        data_unrelated, fk_id_unlrelated = self._create_data_item_and_foreign_key(2, foreign_key_model)
     391
     392        #Delete the data item and make sure the foreign key is affected appropriately
     393        data.delete()
     394        self.assertEqual(None, data.pk)
     395        default_value = default_data()
     396        self.assertEqual(default_value, foreign_key_model.objects.get(id=fk_id_1).fk)
     397        if many_to_one:
     398            self.assertEqual(default_value, foreign_key_model.objects.get(id=fk_id_2).fk)
     399
     400        #Make sure the unrelated data item and foreign key are unaffected
     401        self._test_data_item_and_foreign_key(data_unrelated, 2, foreign_key_model, fk_id_unlrelated)
     402       
     403    def _combined_test(self, cascade_model, protect_model, set_null_model, set_default_model):
     404        #Create data items and foreign key
     405        data, fk_id_cascade = self._create_data_item_and_foreign_key(1, cascade_model)
     406        _, fk_id_protect = self._create_data_item_and_foreign_key(1, protect_model)
     407        _, fk_id_set_null = self._create_data_item_and_foreign_key(1, set_null_model)
     408        _, fk_id_set_default = self._create_data_item_and_foreign_key(1, set_default_model)
     409        data_id = data.pk
     410       
     411        #Attempt to delete--should do nothing due to PROTECT foreign key
     412        self.assertRaises(IntegrityError, data.delete)
     413        self.assertNotEqual(None, data.pk)
     414        self._test_data_item_and_foreign_key(data, 1, cascade_model, fk_id_cascade)
     415        self._test_data_item_and_foreign_key(data, 1, protect_model, fk_id_protect)
     416        self._test_data_item_and_foreign_key(data, 1, set_null_model, fk_id_set_null)
     417        self._test_data_item_and_foreign_key(data, 1, set_default_model, fk_id_set_default)
     418       
     419        #Delete protect foreign key and try again--this time, it should work
     420        fk = protect_model.objects.get(id=fk_id_protect)
     421        fk.delete()
     422        data = Data.objects.get(pk=data_id)
     423        data.delete()
     424        self.assertEqual(None, data.pk)
     425        self.assertRaises(cascade_model.DoesNotExist, cascade_model.objects.get, id=fk_id_cascade)
     426        self.assertEqual(None, set_null_model.objects.get(id=fk_id_set_null).fk)
     427        self.assertEqual(None, set_null_model.objects.get(id=fk_id_set_null).fk_id)
     428        self.assertEqual(default_data(), set_default_model.objects.get(id=fk_id_set_default).fk)
     429
     430    def _create_data_item_and_foreign_key(self, data_value, fk_model):
     431        data, _ = Data.objects.get_or_create(data=data_value)
     432        fk = fk_model.objects.create(fk=data)
     433        self._test_data_item_and_foreign_key(data, data_value, fk_model, fk.id)
     434        return data, fk.id
     435
     436    def _test_data_item_and_foreign_key(self, data_item, data_value, fk_model, fk_id):
     437        data = Data.objects.get(data=data_value)
     438        if data_item:
     439            self.assertEqual(data.pk, data_item.pk)
     440            self.assertEqual(data.data, data_item.data)
     441        fk = fk_model.objects.get(id=fk_id)
     442        self.assertEqual(data, fk.fk)
     443 No newline at end of file
  • tests/modeltests/on_delete_django/models.py

    Property changes on: tests/modeltests/on_delete_django/tests.py
    ___________________________________________________________________
    Name: svn:keywords
       + Id
    
     
     1"""
     2Test ON DELETE behavior for foreign keys
     3when it's handled by the Django.
     4"""
     5
     6from django.db import models
     7
     8### Data model
     9
     10class Data(models.Model):
     11    data = models.IntegerField(unique=True)   
     12
     13def default_data():
     14    data, _ = Data.objects.get_or_create(data=1000)
     15    return data
     16
     17### ForeignKey models
     18
     19class ForeignKey_CASCADE(models.Model):
     20    fk = models.ForeignKey(Data, on_delete=models.CASCADE)
     21
     22class ForeignKey_PROTECT(models.Model):
     23    fk = models.ForeignKey(Data, on_delete=models.PROTECT)
     24
     25class ForeignKey_SET_NULL(models.Model):
     26    fk = models.ForeignKey(Data, null=True, on_delete=models.SET_NULL)
     27
     28class ForeignKey_SET_DEFAULT(models.Model):
     29    fk = models.ForeignKey(Data, default=default_data, on_delete=models.SET_DEFAULT)
     30
     31class ForeignKey_None_Null_True(models.Model):
     32    fk = models.ForeignKey(Data, null=True, on_delete=None)
     33
     34class ForeignKey_None_Null_False(models.Model):
     35    fk = models.ForeignKey(Data, on_delete=None)
     36
     37### OneToOneField models
     38
     39class OneToOne_CASCADE(models.Model):
     40    fk = models.OneToOneField(Data, on_delete=models.CASCADE)
     41
     42class OneToOne_PROTECT(models.Model):
     43    fk = models.OneToOneField(Data, on_delete=models.PROTECT)
     44
     45class OneToOne_SET_NULL(models.Model):
     46    fk = models.OneToOneField(Data, null=True, on_delete=models.SET_NULL)
     47
     48class OneToOne_SET_DEFAULT(models.Model):
     49    fk = models.OneToOneField(Data, default=default_data, on_delete=models.SET_DEFAULT)
     50
     51class OneToOne_None_Null_True(models.Model):
     52    fk = models.OneToOneField(Data, null=True, on_delete=None)
     53
     54class OneToOne_None_Null_False(models.Model):
     55    fk = models.OneToOneField(Data, on_delete=None)
     56   
     57### Multiple FK models
     58
     59class All_FK_Model(models.Model):
     60    fk_ForeignKey_Cascade = models.ForeignKey(Data, null=True, on_delete=models.CASCADE, related_name='fk1')
     61    fk_ForeignKey_Protect = models.ForeignKey(Data, null=True, on_delete=models.PROTECT, related_name='fk2')
     62    fk_ForeignKey_Set_Null = models.ForeignKey(Data, null=True, on_delete=models.SET_NULL, related_name='fk3')
     63    fk_ForeignKey_Set_Default = models.ForeignKey(Data, null=True, default=default_data, on_delete=models.SET_DEFAULT, related_name='fk4')
     64    fk_OneToOneField_Cascade = models.OneToOneField(Data, null=True, on_delete=models.CASCADE, related_name='fk5')
     65    fk_OneToOneField_Protect = models.OneToOneField(Data, null=True, on_delete=models.PROTECT, related_name='fk6')
     66    fk_OneToOneField_Set_Null = models.OneToOneField(Data, null=True, on_delete=models.SET_NULL, related_name='fk7')
     67    fk_OneToOneField_Set_Default = models.OneToOneField(Data, null=True, default=default_data, on_delete=models.SET_DEFAULT, related_name='fk8')
     68 No newline at end of file
  • tests/modeltests/delete/models.py

    Property changes on: tests/modeltests/on_delete_django/models.py
    ___________________________________________________________________
    Name: svn:keywords
       + Id
    
     
    4646
    4747## First, test the CollectedObjects data structure directly
    4848
    49 >>> from django.db.models.query import CollectedObjects
     49>>> from django.db.models.query_utils import CollectedFields, CollectedObjects
    5050
    5151>>> g = CollectedObjects()
    5252>>> g.add("key1", 1, "item1", None)
     
    112112>>> d1 = D(c=c1, a=a1)
    113113>>> d1.save()
    114114
    115 >>> o = CollectedObjects()
    116 >>> a1._collect_sub_objects(o)
     115>>> o, f = CollectedObjects(), CollectedFields()
     116>>> a1._collect_sub_objects(o, f)
    117117>>> o.keys()
    118118[<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>]
     119>>> f.keys()
     120[]
    119121>>> a1.delete()
    120122
    121123# Same again with a known bad order
     
    131133>>> d2 = D(c=c2, a=a2)
    132134>>> d2.save()
    133135
    134 >>> o = CollectedObjects()
    135 >>> a2._collect_sub_objects(o)
     136>>> o, f = CollectedObjects(), CollectedFields()
     137>>> a2._collect_sub_objects(o, f)
    136138>>> o.keys()
    137139[<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>]
     140>>> f.keys()
     141[]
    138142>>> a2.delete()
    139143
    140144### Tests for models E,F - nullable related fields ###
     
    163167# Since E.f is nullable, we should delete F first (after nulling out
    164168# the E.f field), then E.
    165169
    166 >>> o = CollectedObjects()
    167 >>> e1._collect_sub_objects(o)
     170>>> o, f = CollectedObjects(), CollectedFields()
     171>>> e1._collect_sub_objects(o, f)
    168172>>> o.keys()
    169173[<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>]
     174>>> f.keys()
     175[<class 'modeltests.delete.models.E'>]
    170176
    171 # temporarily replace the UpdateQuery class to verify that E.f is actually nulled out first
    172 >>> import django.db.models.sql
    173 >>> class LoggingUpdateQuery(django.db.models.sql.UpdateQuery):
    174 ...     def clear_related(self, related_field, pk_list):
    175 ...         print "CLEARING FIELD",related_field.name
    176 ...         return super(LoggingUpdateQuery, self).clear_related(related_field, pk_list)
    177 >>> original_class = django.db.models.sql.UpdateQuery
    178 >>> django.db.models.sql.UpdateQuery = LoggingUpdateQuery
    179177>>> e1.delete()
    180 CLEARING FIELD f
    181178
    182179>>> e2 = E()
    183180>>> e2.save()
     
    188185
    189186# Same deal as before, though we are starting from the other object.
    190187
    191 >>> o = CollectedObjects()
    192 >>> f2._collect_sub_objects(o)
     188>>> o, f = CollectedObjects(), CollectedFields()
     189>>> f2._collect_sub_objects(o, f)
    193190>>> o.keys()
    194 [<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>]
     191[<class 'modeltests.delete.models.F'>]
     192>>> f.keys()
     193[<class 'modeltests.delete.models.E'>]
    195194
    196195>>> f2.delete()
    197 CLEARING FIELD f
    198 
    199 # Put this back to normal
    200 >>> django.db.models.sql.UpdateQuery = original_class
    201196"""
    202197}
Back to Top