Commits

Andrew Godwin committed 9faade7

Fixed #763: on_delete handling. Thanks to George Lund

Comments (0)

Files changed (3)

south/modelsinspector.py

 from django.db import models
 from django.db.models.base import ModelBase, Model
 from django.db.models.fields import NOT_PROVIDED
+from django.db.models import CASCADE, PROTECT, SET, SET_NULL, SET_DEFAULT, DO_NOTHING
 from django.conf import settings
 from django.utils.functional import Promise
 from django.contrib.contenttypes import generic
 except ImportError:
     timezone = False
 
+def convert_on_delete_handler(value):
+    django_db_models_module = 'models' # relative to standard import 'django.db'
+    if django_db_models_module:
+        if value in (CASCADE, PROTECT, DO_NOTHING, SET_DEFAULT):
+            # straightforward functions
+            return '%s.%s' % (django_db_models_module, value.__name__)
+        else:
+            # This is totally dependent on the implementation of django.db.models.deletion.SET
+            func_name = getattr(value, '__name__', None)
+            if func_name == 'set_on_delete':
+                # we must inspect the function closure to see what parameters were passed in
+                closure_contents = value.func_closure[0].cell_contents
+                if closure_contents is None:
+                    return "%s.SET_NULL" % (django_db_models_module)
+                # simple function we can perhaps cope with:
+                elif hasattr(closure_contents, '__call__'):
+                    # module_name = getattr(closure_contents, '__module__', None)
+                    # inner_func_name = getattr(closure_contents, '__name__', None)
+                    # if inner_func_name:
+                        # TODO there is no way of checking that module_name matches the
+                        # model file, which is the only code that will be imported in
+                        # the Fake ORM. Any other functions won't be available.
+                        # TODO this doesn't work anyway yet as even the app's models
+                        # file is not imported, contrary to the coments in
+                        # orm.LazyFakeORM.eval_in_context, which implies that
+                        # migrations are expected to import that.
+                        # return "%s.SET(%s)" % (django_db_models_module, inner_func_name)
+                    raise ValueError("Function for on_delete could not be serialized.")
+                else:
+                    # an actual value rather than a sentinel function - insanity
+                    raise ValueError("on_delete=SET with a model instance is not supported.")
+                    
+    raise ValueError("%s was not recognized as a valid model deletion handler. Possible values: %s." % (value, ', '.join(f.__name__ for f in (CASCADE, PROTECT, SET, SET_NULL, SET_DEFAULT, DO_NOTHING))))
+
 # Gives information about how to introspect certain fields.
 # This is a list of triples; the first item is a list of fields it applies to,
 # (note that isinstance is used, so superclasses are perfectly valid here)
             "to_field": ["rel.field_name", {"default_attr": "rel.to._meta.pk.name"}],
             "related_name": ["rel.related_name", {"default": None}],
             "db_index": ["db_index", {"default": True}],
+            "on_delete": ["rel.on_delete", {"default": CASCADE, "is_django_function": True, "converter": convert_on_delete_handler, }],
         },
     ),
     (
                 raise IsDefault
             else:
                 raise
+            
     # Lazy-eval functions get eval'd.
     if isinstance(value, Promise):
         value = unicode(value)
     if isinstance(value, Promise):
         value = unicode(value)
     # Callables get called.
-    if callable(value) and not isinstance(value, ModelBase):
+    if not options.get('is_django_function', False) and callable(value) and not isinstance(value, ModelBase):
         # Datetime.datetime.now is special, as we can access it from the eval
         # context (and because it changes all the time; people will file bugs otherwise).
         if value == datetime.datetime.now:
     if "converter" in options:
         value = options['converter'](value)
     # Return the final value
-    return repr(value)
-
+    if options.get('is_django_function', False):
+        return value
+    else:
+        return repr(value)
 
 def introspector(field):
     """

south/tests/fakeapp/models.py

 # An empty case.
 class Other1(models.Model): pass
 
+# Another one
+class Other3(models.Model): pass
+def get_sentinel_object():
+    """
+    A function to return the object to be used in place of any deleted object,
+    when using the SET option for on_delete.
+    """
+    # Create a new one, so we always have an instance to test with. Can't work!
+    return Other3()
+
 # Nastiness.
 class HorribleModel(models.Model):
     "A model to test the edge cases of model parsing"
     o1 = models.ForeignKey(Other1)
     o2 = models.ForeignKey('Other2')
     
+    o_set_null_on_delete = models.ForeignKey('Other3', null=True, on_delete=models.SET_NULL)
+    o_cascade_delete = models.ForeignKey('Other3', null=True, on_delete=models.CASCADE, related_name="cascademe")
+    o_protect = models.ForeignKey('Other3', null=True, on_delete=models.PROTECT, related_name="dontcascademe")
+    o_default_on_delete = models.ForeignKey('Other3', null=True, default=1, on_delete=models.SET_DEFAULT, related_name="setmedefault")
+    o_set_on_delete_function = models.ForeignKey('Other3', null=True, default=1, on_delete=models.SET(get_sentinel_object), related_name="setsentinel")
+    o_set_on_delete_value = models.ForeignKey('Other3', null=True, default=1, on_delete=models.SET(get_sentinel_object()), related_name="setsentinelwithactualvalue") # dubious case
+    o_no_action_on_delete = models.ForeignKey('Other3', null=True, default=1, on_delete=models.DO_NOTHING, related_name="deletemeatyourperil")
+    
+    
     # Now to something outside
     user = models.ForeignKey(UserAlias, related_name="horribles")
     
     multiline = \
               models.TextField(
         )
-    
+
 # Special case.
 class Other2(models.Model):
     # Try loading a field without a newline after it (inspect hates this)

south/tests/inspector.py

         name = HorribleModel._meta.get_field_by_name("name")[0]
         slug = HorribleModel._meta.get_field_by_name("slug")[0]
         user = HorribleModel._meta.get_field_by_name("user")[0]
+        o_set_null_on_delete = HorribleModel._meta.get_field_by_name("o_set_null_on_delete")[0]
+        o_cascade_delete = HorribleModel._meta.get_field_by_name("o_cascade_delete")[0]
+        o_protect = HorribleModel._meta.get_field_by_name("o_protect")[0]
+        o_default_on_delete = HorribleModel._meta.get_field_by_name("o_default_on_delete")[0]
+        o_set_on_delete_function = HorribleModel._meta.get_field_by_name("o_set_on_delete_function")[0]
+        o_set_on_delete_value = HorribleModel._meta.get_field_by_name("o_set_on_delete_value")[0]
+        o_no_action_on_delete = HorribleModel._meta.get_field_by_name("o_no_action_on_delete")[0]
         
         # Simple int retrieval
         self.assertEqual(
             slug,
             ["unique", {"default": True}],
         )
-    
+        
+        # TODO this is repeated from the introspection_details in modelsinspector:
+        # better to refactor that so we can reference these settings, in case they
+        # must change at some point.
+        on_delete = ["rel.on_delete", {"default": CASCADE, "is_django_function": True, "converter": convert_on_delete_handler, }]
+        
+        # Foreign Key cascade update/delete
+        self.assertRaises(
+            IsDefault,
+            get_value,
+            o_cascade_delete,
+            on_delete,
+        )
+        self.assertEqual(
+            get_value(o_protect, on_delete),
+            "models.PROTECT",
+        )
+        self.assertEqual(
+            get_value(o_no_action_on_delete, on_delete),
+            "models.DO_NOTHING",
+        )
+        self.assertEqual(
+            get_value(o_set_null_on_delete, on_delete),
+            "models.SET_NULL",
+        )
+        self.assertEqual(
+            get_value(o_default_on_delete, on_delete),
+            "models.SET_DEFAULT",
+        )
+        # For now o_set_on_delete raises, see modelsinspector.py
+        #self.assertEqual(
+        #    get_value(o_set_on_delete_function, on_delete),
+        #    "models.SET(get_sentinel_object)",
+        #)
+        self.assertRaises(
+            ValueError,
+            get_value,
+            o_set_on_delete_function,
+            on_delete,
+        )
+        self.assertRaises(
+            ValueError,
+            get_value,
+            o_set_on_delete_value, # setting to a specific value not supported
+            on_delete,
+        )
+        
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.