Ticket #7539: on_delete_on_update.diff

File on_delete_on_update.diff, 39.2 KB (added by glassfordm, 15 years ago)

Updated patch for the latest version of Django (revision 9845 at the time the patch was created).

  • django/db/models/base.py

     
    1212from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError
    1313from django.db.models.fields import AutoField, FieldDoesNotExist
    1414from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
    15 from django.db.models.query import delete_objects, Q, CollectedObjects
     15from django.db.models.query import delete_objects, Q, CollectedFields, CollectedObjects
    1616from django.db.models.options import Options
    17 from django.db import connection, transaction, DatabaseError
     17from django.db import connection, transaction, DatabaseError, IntegrityError
    1818from django.db.models import signals
     19from django.db.models.fields.related import CASCADE, RESTRICT, SET_NULL
    1920from django.db.models.loading import register_models, get_model
    2021from django.utils.functional import curry
    2122from django.utils.encoding import smart_str, force_unicode, smart_unicode
     
    412413
    413414    save_base.alters_data = True
    414415
    415     def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):
     416    def _collect_sub_objects(self, seen_objs, fields_to_null, parent=None, nullable=False):
    416417        """
    417418        Recursively populates seen_objs with all objects related to this
    418419        object.
     
    424425        pk_val = self._get_pk_val()
    425426        if seen_objs.add(self.__class__, pk_val, self, parent, nullable):
    426427            return
     428           
     429        if not getattr(settings, 'ON_DELETE_HANDLED_BY_DB', False):
     430            ON_DELETE_NONE_HANDLING = getattr(settings, 'ON_DELETE_NONE_HANDLING', CASCADE)
    427431
    428         for related in self._meta.get_all_related_objects():
    429             rel_opts_name = related.get_accessor_name()
    430             if isinstance(related.field.rel, OneToOneRel):
    431                 try:
    432                     sub_obj = getattr(self, rel_opts_name)
    433                 except ObjectDoesNotExist:
    434                     pass
     432            def _handle_sub_obj(related, sub_obj):
     433                on_delete = related.field.rel.on_delete
     434                if on_delete is None:
     435                    on_delete = ON_DELETE_NONE_HANDLING
     436                   
     437                if on_delete == CASCADE:
     438                    sub_obj._collect_sub_objects(seen_objs, fields_to_null, self.__class__)
     439                elif on_delete == RESTRICT:
     440                    msg = '[Django] Cannot delete a parent object: a foreign key constraint fails (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % (
     441                        sub_obj.__class__,
     442                        sub_obj._get_pk_val(),
     443                        self.__class__,
     444                        pk_val,
     445                        )
     446                    raise IntegrityError(msg) #TODO: include error number also? E.g., "raise IntegrityError(1451, msg)" (but 1451 is MySQL-specific, I believe)
     447                elif on_delete == SET_NULL:
     448                    if not related.field.null:
     449                        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`))' % (
     450                            sub_obj.__class__,
     451                            sub_obj._get_pk_val(),
     452                            self.__class__,
     453                            pk_val,
     454                            )
     455                        raise IntegrityError(msg) #TODO: include error number also? E.g., "raise IntegrityError(<errno>, msg)" (but the error numbers are db-specific, I believe)
     456                    fields_to_null.add(sub_obj.__class__, sub_obj._get_pk_val(), sub_obj, related.field.name)
    435457                else:
    436                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
    437             else:
    438                 for sub_obj in getattr(self, rel_opts_name).all():
    439                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
     458                    raise AttributeError('Unexpected value for on_delete')
    440459
    441         # Handle any ancestors (for the model-inheritance case). We do this by
    442         # traversing to the most remote parent classes -- those with no parents
    443         # themselves -- and then adding those instances to the collection. That
    444         # will include all the child instances down to "self".
    445         parent_stack = self._meta.parents.values()
    446         while parent_stack:
    447             link = parent_stack.pop()
    448             parent_obj = getattr(self, link.name)
    449             if parent_obj._meta.parents:
    450                 parent_stack.extend(parent_obj._meta.parents.values())
    451                 continue
    452             # At this point, parent_obj is base class (no ancestor models). So
    453             # delete it and all its descendents.
    454             parent_obj._collect_sub_objects(seen_objs)
     460            for related in self._meta.get_all_related_objects():
     461                rel_opts_name = related.get_accessor_name()
     462                if isinstance(related.field.rel, OneToOneRel):
     463                    try:
     464                        sub_obj = getattr(self, rel_opts_name)
     465                    except ObjectDoesNotExist:
     466                        pass
     467                    else:
     468                        _handle_sub_obj(related, sub_obj)
     469                else:
     470                    for sub_obj in getattr(self, rel_opts_name).all():
     471                        _handle_sub_obj(related, sub_obj)
    455472
     473            # Handle any ancestors (for the model-inheritance case). We do this by
     474            # traversing to the most remote parent classes -- those with no parents
     475            # themselves -- and then adding those instances to the collection. That
     476            # will include all the child instances down to "self".
     477            parent_stack = self._meta.parents.values()
     478            while parent_stack:
     479                link = parent_stack.pop()
     480                parent_obj = getattr(self, link.name)
     481                if parent_obj._meta.parents:
     482                    parent_stack.extend(parent_obj._meta.parents.values())
     483                    continue
     484                # At this point, parent_obj is base class (no ancestor models). So
     485                # delete it and all its descendents.
     486                parent_obj._collect_sub_objects(seen_objs, fields_to_null)
     487
    456488    def delete(self):
    457489        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)
    458490
    459491        # Find all the objects than need to be deleted.
    460492        seen_objs = CollectedObjects()
    461         self._collect_sub_objects(seen_objs)
     493        fields_to_null = CollectedFields()
     494        self._collect_sub_objects(seen_objs, fields_to_null)
    462495
    463496        # Actually delete the objects.
    464         delete_objects(seen_objs)
     497        delete_objects(seen_objs, fields_to_null)
    465498
    466499    delete.alters_data = True
    467500
  • django/db/models/fields/related.py

     
    1010from django.utils.functional import curry
    1111from django.core import exceptions
    1212from django import forms
     13from django.conf import settings
    1314
    1415try:
    1516    set
     
    2021
    2122pending_lookups = {}
    2223
     24class _OnDeleteOrUpdateAction(object):
     25    pass
     26class RESTRICT(_OnDeleteOrUpdateAction):
     27    sql = 'RESTRICT'
     28class CASCADE(_OnDeleteOrUpdateAction):
     29    sql = 'CASCADE'
     30class SET_NULL(_OnDeleteOrUpdateAction):
     31    sql = 'SET NULL'
     32ALLOWED_ON_DELETE_ACTION_TYPES = set([None, CASCADE, RESTRICT, SET_NULL])
     33ALLOWED_ON_UPDATE_ACTION_TYPES = ALLOWED_ON_DELETE_ACTION_TYPES.copy()
     34
    2335def add_lazy_relation(cls, field, relation, operation):
    2436    """
    2537    Adds a lookup on ``cls`` when a related field is defined using a string,
     
    584596
    585597class ManyToOneRel(object):
    586598    def __init__(self, to, field_name, related_name=None,
    587             limit_choices_to=None, lookup_overrides=None, parent_link=False):
     599            limit_choices_to=None, lookup_overrides=None, parent_link=False,
     600            on_delete=None, on_update=None):
    588601        try:
    589602            to._meta
    590603        except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
     
    597610        self.lookup_overrides = lookup_overrides or {}
    598611        self.multiple = True
    599612        self.parent_link = parent_link
     613        self.on_delete = on_delete
     614        self.on_update = on_update
    600615
    601616    def get_related_field(self):
    602617        """
     
    611626
    612627class OneToOneRel(ManyToOneRel):
    613628    def __init__(self, to, field_name, related_name=None,
    614             limit_choices_to=None, lookup_overrides=None, parent_link=False):
     629            limit_choices_to=None, lookup_overrides=None, parent_link=False,
     630            on_delete=None, on_update=None):
    615631        super(OneToOneRel, self).__init__(to, field_name,
    616632                related_name=related_name, limit_choices_to=limit_choices_to,
    617                 lookup_overrides=lookup_overrides, parent_link=parent_link)
     633                lookup_overrides=lookup_overrides, parent_link=parent_link,
     634                on_delete=on_delete, on_update=on_update)
    618635        self.multiple = False
    619636
    620637class ManyToManyRel(object):
     
    645662            related_name=kwargs.pop('related_name', None),
    646663            limit_choices_to=kwargs.pop('limit_choices_to', None),
    647664            lookup_overrides=kwargs.pop('lookup_overrides', None),
    648             parent_link=kwargs.pop('parent_link', False))
     665            parent_link=kwargs.pop('parent_link', False),
     666            on_delete=kwargs.pop('on_delete', None),
     667            on_update=kwargs.pop('on_update', None))
    649668        Field.__init__(self, **kwargs)
    650669
    651670        self.db_index = True
     
    690709            target = self.rel.to._meta.db_table
    691710        cls._meta.duplicate_targets[self.column] = (target, "o2m")
    692711
     712        on_delete, on_update = self.rel.on_delete, self.rel.on_update
     713        if on_delete not in ALLOWED_ON_DELETE_ACTION_TYPES:
     714            raise ValueError("Invalid value 'on_delete=%s' specified for ForeignKey field %s.%s." % (on_delete, cls.__name__, name))
     715        if on_update not in ALLOWED_ON_UPDATE_ACTION_TYPES:
     716            raise ValueError("Invalid value 'on_update=%s' specified for ForeignKey field %s.%s." % (on_update, cls.__name__, name))
     717        if (on_delete == SET_NULL or on_update == SET_NULL) and not self.null:
     718            if on_delete == SET_NULL and on_update == SET_NULL:
     719                specification = "'on_delete=SET_NULL' and 'on_update=SET_NULL'"
     720            elif on_delete == SET_NULL:
     721                specification = "'on_delete=SET_NULL'"
     722            else:
     723                specification = "'on_update=SET_NULL'"
     724            raise ValueError("%s specified for ForeignKey field '%s.%s', but the field is not nullable." % (specification, cls.__name__, name))
     725        if on_delete is None and getattr(settings, 'ON_DELETE_NONE_HANDLING', CASCADE) == SET_NULL and not self.null:
     726            raise ValueError("on_delete=SET_NULL is being used by default (based on the ON_DELETE_NONE_HANDLING setting) for ForeignKey field '%s.%s', but the field is not nullable." % (specification, cls.__name__, name))
     727
    693728    def contribute_to_related_class(self, cls, related):
    694729        setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related))
    695730
  • 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, RESTRICT, SET_NULL
    1415from django.db.models import signals
    1516
    1617# Admin stages.
  • django/db/models/query.py

     
    117117        """
    118118        return self.data.keys()
    119119
     120class CollectedFields(object):
     121    """
     122    A container that stores model objects and fields that need to
     123    be nulled to enforce the on_delete=SET_NULL ForeignKey constraint.
     124    """
    120125
     126    def __init__(self):
     127        self.data = {}
     128
     129    def add(self, model, pk, obj, field_name):
     130        """
     131        Adds an item.
     132        model is the class of the object being added,
     133        pk is the primary key, obj is the object itself,
     134        field_name is the name of the field to be nulled.
     135        """
     136        d = self.data.setdefault(model, SortedDict())
     137        obj, field_names = d.setdefault(pk, (obj, set()))
     138        field_names.add(field_name)
     139
     140    def __contains__(self, key):
     141        return self.data.__contains__(key)
     142
     143    def __getitem__(self, key):
     144        return self.data[key]
     145
     146    def __nonzero__(self):
     147        return bool(self.data)
     148
     149    def iteritems(self):
     150        return self.data.iteritems()
     151
     152    def iterkeys(self):
     153        return self.data.iterkeys()
     154
     155    def itervalues(self):
     156        return self.data.itervalues()
     157       
    121158class QuerySet(object):
    122159    """
    123160    Represents a lazy database lookup for a set of objects.
     
    427464            # Collect all the objects to be deleted in this chunk, and all the
    428465            # objects that are related to the objects that are to be deleted.
    429466            seen_objs = CollectedObjects()
     467            fields_to_null = CollectedFields()
    430468            for object in del_query[:CHUNK_SIZE]:
    431                 object._collect_sub_objects(seen_objs)
     469                object._collect_sub_objects(seen_objs, fields_to_null)
    432470
    433471            if not seen_objs:
    434472                break
    435             delete_objects(seen_objs)
     473            delete_objects(seen_objs, fields_to_null)
    436474
    437475        # Clear the result cache, in case this QuerySet gets reused.
    438476        self._result_cache = None
     
    951989                setattr(obj, f.get_cache_name(), rel_obj)
    952990    return obj, index_end
    953991
    954 
    955 def delete_objects(seen_objs):
     992def delete_objects(seen_objs, fields_to_null):
    956993    """
    957994    Iterate through a list of seen classes, and remove any instances that are
    958995    referred to.
     
    9921029                    update_query.clear_related(field, pk_list)
    9931030
    9941031    # Now delete the actual data.
     1032    for cls, cls_dct in fields_to_null.iteritems():
     1033        update_query = sql.UpdateQuery(cls, connection)
     1034        field_dict = {}
     1035        for pk, (_, field_names) in cls_dct.iteritems():
     1036            for field_name in field_names:
     1037                pk_set = field_dict.setdefault(field_name, set())
     1038                pk_set.add(pk)
     1039        for field_name, pk_set in field_dict.iteritems():
     1040            update_query.clear_related(cls._meta.get_field_by_name(field_name)[0], list(pk_set))
    9951041    for cls in ordered_classes:
    9961042        items = obj_pairs[cls]
    9971043        items.reverse()
     
    10031049        # Last cleanup; set NULLs where there once was a reference to the
    10041050        # object, NULL the primary key of the found objects, and perform
    10051051        # post-notification.
     1052        for cls, cls_dct in fields_to_null.iteritems():
     1053            for instance, field_names in cls_dct.itervalues():
     1054                for field_name in field_names:
     1055                    field = cls._meta.get_field_by_name(field_name)[0]
     1056                    setattr(instance, field.attname, None)
    10061057        for pk_val, instance in items:
    10071058            for field in cls._meta.fields:
    10081059                if field.rel and field.null and field.rel.to in seen_objs:
  • django/db/backends/creation.py

     
    9494        "Return the SQL snippet defining the foreign key reference for a field"
    9595        qn = self.connection.ops.quote_name
    9696        if field.rel.to in known_models:
     97            on_delete_clause, on_update_clause = self.on_delete_and_update_clauses(style, field)
    9798            output = [style.SQL_KEYWORD('REFERENCES') + ' ' + \
    9899                style.SQL_TABLE(qn(field.rel.to._meta.db_table)) + ' (' + \
    99100                style.SQL_FIELD(qn(field.rel.to._meta.get_field(field.rel.field_name).column)) + ')' +
     101                on_delete_clause + on_update_clause +
    100102                self.connection.ops.deferrable_sql()
    101103            ]
    102104            pending = False
     
    117119        opts = model._meta
    118120        if model in pending_references:
    119121            for rel_class, f in pending_references[model]:
     122                on_delete_clause, on_update_clause = self.on_delete_and_update_clauses(style, f)
    120123                rel_opts = rel_class._meta
    121124                r_table = rel_opts.db_table
    122125                r_col = f.column
     
    125128                # For MySQL, r_name must be unique in the first 64 characters.
    126129                # So we are careful with character usage here.
    127130                r_name = '%s_refs_%s_%x' % (r_col, col, abs(hash((r_table, table))))
    128                 final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % \
     131                final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s%s%s;' % \
    129132                    (qn(r_table), qn(truncate_name(r_name, self.connection.ops.max_name_length())),
    130133                    qn(r_col), qn(table), qn(col),
     134                    on_delete_clause, on_update_clause,
    131135                    self.connection.ops.deferrable_sql()))
    132136            del pending_references[model]
    133137        return final_output
    134138
     139    def on_delete_and_update_clauses(self, style, foreign_key_field):
     140        on_delete_clause = ''
     141        try:
     142            on_delete = getattr(foreign_key_field, 'ON_DELETE_HANDLED_BY_DB') #USED FOR UNIT TESTING ONLY (see comment in modeltests/on_delete_and_update_db/models.py)
     143        except AttributeError:
     144            on_delete = getattr(settings, 'ON_DELETE_HANDLED_BY_DB', False)
     145        if on_delete:
     146            on_delete = getattr(foreign_key_field.rel, 'on_delete', None)
     147            if on_delete:
     148                on_delete_clause = style.SQL_KEYWORD(' ON DELETE %s' % foreign_key_field.rel.on_delete.sql)
     149           
     150        on_update_clause = ''
     151        try:
     152            on_update = getattr(foreign_key_field, 'ON_UPDATE_HANDLED_BY_DB') #USED FOR UNIT TESTING ONLY; (see comment in modeltests/on_delete_and_update_db/models.py)
     153        except AttributeError:
     154            on_update = getattr(settings, 'ON_UPDATE_HANDLED_BY_DB', False)
     155        if on_update:
     156            on_update = getattr(foreign_key_field.rel, 'on_update', None)
     157            if on_update:
     158                on_update_clause = style.SQL_KEYWORD(' ON UPDATE %s' % foreign_key_field.rel.on_update.sql)
     159
     160        return on_delete_clause, on_update_clause
     161       
    135162    def sql_for_many_to_many(self, model, style):
    136163        "Return the CREATE TABLE statments for all the many-to-many tables defined on a model"
    137164        output = []
  • tests/modeltests/on_delete_and_on_update_db/tests.py

    Property changes on: tests/modeltests/on_delete_and_on_update_db/__init__.py
    ___________________________________________________________________
    Name: svn:keywords
       + Id
    
     
     1"""
     2Test ON DELETE and ON UPDATE behavior for foreign keys
     3when it's handled by the database
     4(ie. when settings.ON_DELETE_HANDLED_BY_DB=True
     5and settings.ON_UPDATE_HANDLED_BY_DB=True).
     6"""
     7
     8from django.conf import settings
     9from django.db import IntegrityError
     10from django.test import TestCase
     11
     12from models import *
     13
     14
     15class ON_DELETE_Tests(TestCase):
     16    def setUp(self):
     17        self._old = getattr(settings, 'ON_DELETE_HANDLED_BY_DB', False)
     18        settings.ON_DELETE_HANDLED_BY_DB = True
     19
     20    def tearDown(self):
     21        settings.ON_DELETE_HANDLED_BY_DB = self._old
     22
     23    def test_on_delete_SET_NULL_requires_nullable_field(self):
     24        try:
     25            class Test(models.Model):
     26                fk = models.ForeignKey(Data, to_field='data', on_delete=models.SET_NULL)
     27        except ValueError, e:
     28            self.assertEqual(str(e), "'on_delete=SET_NULL' specified for ForeignKey field 'Test.fk', but the field is not nullable.")
     29        else:
     30            self.assertTrue(False, 'Expected exception not raised.')
     31       
     32    def test_on_delete_None(self):
     33        #SQL behavior is to default to RESTRICT if no ON DELETE clause is specified
     34        FKModel = FK_on_delete_None
     35        data = Data.objects.create(data=1)
     36        FKModel.objects.create(fk=data)
     37        self.assertRaises(IntegrityError, data.delete)
     38
     39    def test_on_delete_CASCADE(self):
     40        FKModel = FK_on_delete_CASCADE
     41        data = Data.objects.create(data=1)
     42        fk_id = FKModel.objects.create(fk=data).id
     43        self.assertEqual(data, FKModel.objects.get(id=fk_id).fk)
     44        data.delete()
     45        self.assertRaises(FKModel.DoesNotExist, FKModel.objects.get, id=fk_id)
     46
     47    def test_on_delete_RESTRICT(self):
     48        FKModel = FK_on_delete_RESTRICT
     49        data = Data.objects.create(data=1)
     50        FKModel.objects.create(fk=data)
     51        self.assertRaises(IntegrityError, data.delete)
     52
     53    def test_on_delete_SET_NULL(self):
     54        FKModel = FK_on_delete_SET_NULL
     55        data = Data.objects.create(data=1)
     56        fk_id = FKModel.objects.create(fk=data).id
     57        self.assertEqual(data, FKModel.objects.get(id=fk_id).fk)
     58        self.assertEqual(1, FKModel.objects.get(id=fk_id).fk_id)
     59        data.delete()
     60        self.assertEqual(None, FKModel.objects.get(id=fk_id).fk)
     61        self.assertEqual(None, FKModel.objects.get(id=fk_id).fk_id)
     62
     63
     64class ON_UPDATE_Tests(TestCase):
     65    def setUp(self):
     66        self._old = getattr(settings, 'ON_UPDATE_HANDLED_BY_DB', False)
     67        settings.ON_UPDATE_HANDLED_BY_DB = True
     68
     69    def tearDown(self):
     70        settings.ON_UPDATE_HANDLED_BY_DB = self._old
     71
     72    def test_on_update_SET_NULL_requires_nullable_field(self):
     73        try:
     74            class Test(models.Model):
     75                fk = models.ForeignKey(Data, to_field='data', on_update=models.SET_NULL)
     76        except ValueError, e:
     77            self.assertEqual(str(e), "'on_update=SET_NULL' specified for ForeignKey field 'Test.fk', but the field is not nullable.")
     78        else:
     79            self.assertTrue(False, 'Expected exception not raised.')
     80       
     81    def test_on_update_None(self):
     82        #SQL behavior is to default to RESTRICT if no ON DELETE clause is specified
     83        FKModel = FK_on_update_None
     84        data = Data.objects.create(data=1)
     85        FKModel.objects.create(fk=data)
     86        data.data += 1
     87        self.assertRaises(IntegrityError, data.save)
     88
     89    def test_on_update_CASCADE(self):
     90        FKModel = FK_on_update_CASCADE
     91        data = Data.objects.create(data=1)
     92        fk_id = FKModel.objects.create(fk=data).id
     93        self.assertEqual(1, FKModel.objects.get(id=fk_id).fk_id)
     94        data.data += 1
     95        data.save()
     96        self.assertEqual(2, FKModel.objects.get(id=fk_id).fk_id)
     97
     98    def test_on_update_RESTRICT(self):
     99        FKModel = FK_on_update_RESTRICT
     100        data = Data.objects.create(data=1)
     101        FKModel.objects.create(fk=data)
     102        data.data += 1
     103        self.assertRaises(IntegrityError, data.save)
     104
     105    def test_on_update_SET_NULL(self):
     106        FKModel = FK_on_update_SET_NULL
     107        data = Data.objects.create(data=1)
     108        fk_id = FKModel.objects.create(fk=data).id
     109        self.assertEqual(data, FKModel.objects.get(id=fk_id).fk)
     110        self.assertEqual(1, FKModel.objects.get(id=fk_id).fk_id)
     111        data.data += 1
     112        data.save()
     113        self.assertEqual(None, FKModel.objects.get(id=fk_id).fk)
     114        self.assertEqual(None, FKModel.objects.get(id=fk_id).fk_id)
     115
  • tests/modeltests/on_delete_and_on_update_db/models.py

    Property changes on: tests/modeltests/on_delete_and_on_update_db/tests.py
    ___________________________________________________________________
    Name: svn:keywords
       + Id
    
     
     1"""
     2Test ON DELETE and ON UPDATE behavior for foreign keys
     3when it's handled by the database.
     4"""
     5
     6from django.db import models
     7
     8
     9#Note: all the lines marked "Used for unit testing only" mean:
     10#Some tests (the ones modeltests/on_delete_and_update_db/test.py)
     11#need to force SQL generation to include ON UPDATE and ON DELETE clauses,
     12#while others (the ones modeltests/on_delete_and_update_django/test.py)
     13#need to force the SQL not to include these clauses.
     14#Normally the SQL generation is controlled by the settings.py file;
     15#however, usually the database is created all at once when the unit
     16#tests are started, and it uses the settings at the time that happens;
     17#there is no way (that I found) to have different settings for different
     18#unit tests. So, this method of controlling it field-by-field was
     19#added instead; it is intended to be used only by the unit tests.
     20
     21
     22class Data(models.Model):
     23    data = models.IntegerField(unique=True)
     24
     25
     26class FK_on_delete_None(models.Model):
     27    fk = models.ForeignKey(Data, to_field='data', on_delete=None)
     28    fk.ON_DELETE_HANDLED_BY_DB = True #Used for unit testing only (see comment above)
     29
     30class FK_on_delete_CASCADE(models.Model):
     31    fk = models.ForeignKey(Data, to_field='data', on_delete=models.CASCADE)
     32    fk.ON_DELETE_HANDLED_BY_DB = True #Used for unit testing only (see comment above)
     33
     34class FK_on_delete_RESTRICT(models.Model):
     35    fk = models.ForeignKey(Data, to_field='data', on_delete=models.RESTRICT)
     36    fk.ON_DELETE_HANDLED_BY_DB = True #Used for unit testing only (see comment above)
     37
     38class FK_on_delete_SET_NULL(models.Model):
     39    fk = models.ForeignKey(Data, to_field='data', null=True, on_delete=models.SET_NULL)
     40    fk.ON_DELETE_HANDLED_BY_DB = True #Used for unit testing only (see comment above)
     41
     42
     43class FK_on_update_None(models.Model):
     44    fk = models.ForeignKey(Data, to_field='data', on_update=None)
     45    fk.ON_UPDATE_HANDLED_BY_DB = True #Used for unit testing only (see comment above)
     46
     47class FK_on_update_CASCADE(models.Model):
     48    fk = models.ForeignKey(Data, to_field='data', on_update=models.CASCADE)
     49    fk.ON_UPDATE_HANDLED_BY_DB = True #Used for unit testing only (see comment above)
     50
     51class FK_on_update_RESTRICT(models.Model):
     52    fk = models.ForeignKey(Data, to_field='data', on_update=models.RESTRICT)
     53    fk.ON_UPDATE_HANDLED_BY_DB = True #Used for unit testing only (see comment above)
     54
     55class FK_on_update_SET_NULL(models.Model):
     56    fk = models.ForeignKey(Data, to_field='data', null=True, on_update=models.SET_NULL)
     57    fk.ON_UPDATE_HANDLED_BY_DB = True #Used for unit testing only (see comment above)
  • tests/modeltests/on_delete_and_on_update_django/tests.py

    Property changes on: tests/modeltests/on_delete_and_on_update_db/models.py
    ___________________________________________________________________
    Name: svn:keywords
       + Id
    
    
    Property changes on: tests/modeltests/on_delete_and_on_update_django/__init__.py
    ___________________________________________________________________
    Name: svn:keywords
       + Id
    
     
     1"""
     2Test ON DELETE and ON UPDATE behavior for foreign keys
     3when it's handled by the Django
     4(ie. when settings.ON_DELETE_HANDLED_BY_DB=False
     5and settings.ON_UPDATE_HANDLED_BY_DB=False).
     6"""
     7
     8from django.conf import settings
     9from django.db import IntegrityError
     10from django.db.models.fields.related import CASCADE, RESTRICT, SET_NULL
     11from django.test import TestCase
     12
     13from models import *
     14
     15
     16class ON_DELETE_Tests(TestCase):
     17    def setUp(self):
     18        self._old_ON_DELETE_HANDLED_BY_DB = getattr(settings, 'ON_DELETE_HANDLED_BY_DB', False)
     19        settings.ON_DELETE_HANDLED_BY_DB = False
     20        self._old_ON_DELETE_NONE_HANDLING = getattr(settings, 'ON_DELETE_NONE_HANDLING', CASCADE)
     21   
     22    def tearDown(self):
     23        settings.ON_DELETE_HANDLED_BY_DB = self._old_ON_DELETE_HANDLED_BY_DB
     24        settings.ON_DELETE_NONE_HANDLING = self._old_ON_DELETE_NONE_HANDLING
     25   
     26    def test_on_delete_SET_NULL_requires_nullable_field(self):
     27        try:
     28            class Test(models.Model):
     29                fk = models.ForeignKey(Data, to_field='data', on_delete=models.SET_NULL)
     30        except ValueError, e:
     31            self.assertEqual(str(e), "'on_delete=SET_NULL' specified for ForeignKey field 'Test.fk', but the field is not nullable.")
     32        else:
     33            self.assertTrue(False, 'Expected exception not raised.')
     34   
     35    def test_on_delete_None_CASCADE(self):
     36        FKModel = FK_on_delete_None
     37        settings.ON_DELETE_NONE_HANDLING = CASCADE
     38        data = Data.objects.create(data=1)
     39        fk_id = FKModel.objects.create(fk=data).id
     40        FKModel.objects.get(id=fk_id)
     41        data.delete()
     42        self.assertRaises(FKModel.DoesNotExist, FKModel.objects.get, id=fk_id)
     43   
     44    def test_on_delete_None_SET_NULL(self):
     45        FKModel = FK_on_delete_None
     46        settings.ON_DELETE_NONE_HANDLING = SET_NULL
     47        data = Data.objects.create(data=1)
     48        fk_id = FKModel.objects.create(fk=data).id
     49        self.assertEqual(data, FKModel.objects.get(id=fk_id).fk)
     50        self.assertEqual(1, FKModel.objects.get(id=fk_id).fk_id)
     51        data.delete()
     52        self.assertEqual(None, FKModel.objects.get(id=fk_id).fk)
     53        self.assertEqual(None, FKModel.objects.get(id=fk_id).fk_id)
     54   
     55    def test_on_delete_None_SET_NULL_NoNull(self):
     56        FKModel = FK_on_delete_None_NoNull
     57        settings.ON_DELETE_NONE_HANDLING = SET_NULL
     58        data = Data.objects.create(data=1)
     59        FKModel.objects.create(fk=data).id
     60        self.assertRaises(IntegrityError, data.delete)
     61   
     62    def test_on_delete_None_RESTRICT(self):
     63        FKModel = FK_on_delete_None
     64        settings.ON_DELETE_NONE_HANDLING = RESTRICT
     65        data = Data.objects.create(data=1)
     66        FKModel.objects.create(fk=data)
     67        self.assertRaises(IntegrityError, data.delete)
     68   
     69    def test_on_delete_CASCADE(self):
     70        FKModel = FK_on_delete_CASCADE
     71        data = Data.objects.create(data=1)
     72        fk_id = FKModel.objects.create(fk=data).id
     73        FKModel.objects.get(id=fk_id)
     74        data.delete()
     75        self.assertRaises(FKModel.DoesNotExist, FKModel.objects.get, id=fk_id)
     76   
     77    def test_on_delete_RESTRICT(self):
     78        FKModel = FK_on_delete_RESTRICT
     79        data = Data.objects.create(data=1)
     80        FKModel.objects.create(fk=data)
     81        self.assertRaises(IntegrityError, data.delete)
     82   
     83    def test_on_delete_SET_NULL(self):
     84        FKModel = FK_on_delete_SET_NULL
     85        data = Data.objects.create(data=1)
     86        fk_id = FKModel.objects.create(fk=data).id
     87        self.assertEqual(data, FKModel.objects.get(id=fk_id).fk)
     88        self.assertEqual(1, FKModel.objects.get(id=fk_id).fk_id)
     89        data.delete()
     90        self.assertEqual(None, FKModel.objects.get(id=fk_id).fk)
     91        self.assertEqual(None, FKModel.objects.get(id=fk_id).fk_id)
     92
     93
     94class ON_UPDATE_Tests(TestCase):
     95    def setUp(self):
     96        self._old_ON_UPDATE_HANDLED_BY_DB = getattr(settings, 'ON_UPDATE_HANDLED_BY_DB', False)
     97        settings.ON_UPDATE_HANDLED_BY_DB = False
     98   
     99    def tearDown(self):
     100        settings.ON_UPDATE_HANDLED_BY_DB = self._old_ON_UPDATE_HANDLED_BY_DB
     101   
     102    def test_on_update_SET_NULL_requires_nullable_field(self):
     103        try:
     104            class Test(models.Model):
     105                fk = models.ForeignKey(Data, to_field='data', on_update=models.SET_NULL)
     106        except ValueError, e:
     107            self.assertEqual(str(e), "'on_update=SET_NULL' specified for ForeignKey field 'Test.fk', but the field is not nullable.")
     108        else:
     109            self.assertTrue(False, 'Expected exception not raised.')
     110   
     111    def test_on_update_None(self):
     112        #Currently Django has no ON UPDATE behavior at all,
     113        #so the default SQL behavior when no ON UPDATE clause
     114        #is specified takes over: it acts like ON UPDATE RESTRICT.
     115        FKModel = FK_on_update_None
     116        data = Data.objects.create(data=1)
     117        FKModel.objects.create(fk=data)
     118        data.data += 1
     119        self.assertRaises(IntegrityError, data.save)
     120   
     121    def test_on_update_CASCADE(self):
     122        #Currently Django has no ON UPDATE behavior at all,
     123        #so the default SQL behavior when no ON UPDATE clause
     124        #is specified takes over: it acts like ON UPDATE RESTRICT.
     125        FKModel = FK_on_update_CASCADE
     126        data = Data.objects.create(data=1)
     127        FKModel.objects.create(fk=data)
     128        data.data += 1
     129        self.assertRaises(IntegrityError, data.save)
     130   
     131    def test_on_update_RESTRICT(self):
     132        #Currently Django has no ON UPDATE behavior at all,
     133        #so the default SQL behavior when no ON UPDATE clause
     134        #is specified takes over: it acts like ON UPDATE RESTRICT.
     135        FKModel = FK_on_update_RESTRICT
     136        data = Data.objects.create(data=1)
     137        FKModel.objects.create(fk=data)
     138        data.data += 1
     139        self.assertRaises(IntegrityError, data.save)
     140   
     141    def test_on_update_SET_NULL(self):
     142        #Currently Django has no ON UPDATE behavior at all,
     143        #so the default SQL behavior when no ON UPDATE clause
     144        #is specified takes over: it acts like ON UPDATE RESTRICT.
     145        FKModel = FK_on_update_SET_NULL
     146        data = Data.objects.create(data=1)
     147        FKModel.objects.create(fk=data)
     148        data.data += 1
     149        self.assertRaises(IntegrityError, data.save)
     150 No newline at end of file
  • tests/modeltests/on_delete_and_on_update_django/models.py

    Property changes on: tests/modeltests/on_delete_and_on_update_django/tests.py
    ___________________________________________________________________
    Name: svn:keywords
       + Id
    
     
     1"""
     2Test ON DELETE and ON UPDATE behavior for foreign keys
     3when it's handled by the Django.
     4"""
     5
     6from django.db import models
     7
     8
     9#Note: for the meaning of the lines marked "Used for unit testing only",
     10#see comment in modeltests/on_delete_and_update_db/models.py
     11
     12
     13class Data(models.Model):
     14    data = models.IntegerField(unique=True)
     15
     16
     17class FK_on_delete_None(models.Model):
     18    fk = models.ForeignKey(Data, to_field='data', null=True, on_delete=None)
     19    fk.ON_DELETE_HANDLED_BY_DB = False #Used for unit testing only (see comment above)
     20
     21class FK_on_delete_None_NoNull(models.Model):
     22    fk = models.ForeignKey(Data, to_field='data', on_delete=None)
     23    fk.ON_DELETE_HANDLED_BY_DB = False #Used for unit testing only (see comment above)
     24
     25class FK_on_delete_CASCADE(models.Model):
     26    fk = models.ForeignKey(Data, to_field='data', on_delete=models.CASCADE)
     27    fk.ON_DELETE_HANDLED_BY_DB = False #Used for unit testing only (see comment above)
     28
     29class FK_on_delete_RESTRICT(models.Model):
     30    fk = models.ForeignKey(Data, to_field='data', on_delete=models.RESTRICT)
     31    fk.ON_DELETE_HANDLED_BY_DB = False #Used for unit testing only (see comment above)
     32
     33class FK_on_delete_SET_NULL(models.Model):
     34    fk = models.ForeignKey(Data, to_field='data', null=True, on_delete=models.SET_NULL)
     35    fk.ON_DELETE_HANDLED_BY_DB = False #Used for unit testing only (see comment above)
     36
     37
     38class FK_on_update_None(models.Model):
     39    fk = models.ForeignKey(Data, to_field='data', on_update=None)
     40    fk.ON_UPDATE_HANDLED_BY_DB = False #Used for unit testing only (see comment above)
     41
     42class FK_on_update_CASCADE(models.Model):
     43    fk = models.ForeignKey(Data, to_field='data', on_update=models.CASCADE)
     44    fk.ON_UPDATE_HANDLED_BY_DB = False #Used for unit testing only (see comment above)
     45
     46class FK_on_update_RESTRICT(models.Model):
     47    fk = models.ForeignKey(Data, to_field='data', on_update=models.RESTRICT)
     48    fk.ON_UPDATE_HANDLED_BY_DB = False #Used for unit testing only (see comment above)
     49
     50class FK_on_update_SET_NULL(models.Model):
     51    fk = models.ForeignKey(Data, to_field='data', null=True, on_update=models.SET_NULL)
     52    fk.ON_UPDATE_HANDLED_BY_DB = False #Used for unit testing only (see comment above)
     53   
     54 No newline at end of file
  • tests/modeltests/delete/models.py

    Property changes on: tests/modeltests/on_delete_and_on_update_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 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, p = CollectedObjects(), CollectedFields()
     116>>> a1._collect_sub_objects(o, p)
    117117>>> o.keys()
    118118[<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>]
    119119>>> a1.delete()
     
    131131>>> d2 = D(c=c2, a=a2)
    132132>>> d2.save()
    133133
    134 >>> o = CollectedObjects()
    135 >>> a2._collect_sub_objects(o)
     134>>> o, p = CollectedObjects(), CollectedFields()
     135>>> a2._collect_sub_objects(o, p)
    136136>>> o.keys()
    137137[<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>]
    138138>>> a2.delete()
     
    163163# Since E.f is nullable, we should delete F first (after nulling out
    164164# the E.f field), then E.
    165165
    166 >>> o = CollectedObjects()
    167 >>> e1._collect_sub_objects(o)
     166>>> o, p = CollectedObjects(), CollectedFields()
     167>>> e1._collect_sub_objects(o, p)
    168168>>> o.keys()
    169169[<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>]
    170170
     
    179179
    180180# Same deal as before, though we are starting from the other object.
    181181
    182 >>> o = CollectedObjects()
    183 >>> f2._collect_sub_objects(o)
     182>>> o, p = CollectedObjects(), CollectedFields()
     183>>> f2._collect_sub_objects(o, p)
    184184>>> o.keys()
    185185[<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>]
Back to Top