Django

Code

Ticket #7539: on_delete_on_update.diff

File on_delete_on_update.diff, 39.2 kB (added by glassfordm, 1 year ago)

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

  • django/db/models/base.py

    old new  
    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

    old new  
    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

    old new  
    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

    old new  
    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

    old new  
    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

    old new  
     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

    old new  
     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

    old new  
     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) 
  • tests/modeltests/on_delete_and_on_update_django/models.py

    old new  
     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     
  • tests/modeltests/delete/models.py

    old new  
    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'>]