Anonymous avatar Anonymous committed 9a76825

Fixed #8309: subclasses now inherit `GenericForeignKey` correctly. There's also now an internal API so that other "virtual fields" like GFK can be inherited as well. Thanks, msaelices.

Comments (0)

Files changed (4)

django/contrib/contenttypes/generic.py

         self.fk_field = fk_field
 
     def contribute_to_class(self, cls, name):
-        # Make sure the fields exist (these raise FieldDoesNotExist,
-        # which is a fine error to raise here)
         self.name = name
         self.model = cls
         self.cache_attr = "_%s_cache" % name
+        cls._meta.add_virtual_field(self)
 
         # For some reason I don't totally understand, using weakrefs here doesn't work.
         signals.pre_init.connect(self.instance_pre_init, sender=cls, weak=False)

django/db/models/base.py

                 # Things without _meta aren't functional models, so they're
                 # uninteresting parents.
                 continue
+                
+            # All the fields of any type declared on this model
+            new_fields = new_class._meta.local_fields + \
+                         new_class._meta.many_to_many + \
+                         new_class._meta.virtual_fields
+            field_names = set([f.name for f in new_fields])
+                
+            # Concrete classes...
             if not base._meta.abstract:
                 if base in o2o_map:
                     field = o2o_map[base]
                             auto_created=True, parent_link=True)
                     new_class.add_to_class(attr_name, field)
                 new_class._meta.parents[base] = field
+            
+            # .. and abstract ones.
             else:
-                # The abstract base class case.
-                names = set([f.name for f in new_class._meta.local_fields + new_class._meta.many_to_many])
-                for field in base._meta.local_fields + base._meta.local_many_to_many:
-                    if field.name in names:
-                        raise FieldError('Local field %r in class %r clashes with field of similar name from abstract base class %r'
-                                % (field.name, name, base.__name__))
+                # Check for clashes between locally declared fields and those on the ABC.
+                parent_fields = base._meta.local_fields + base._meta.local_many_to_many
+                for field in parent_fields:
+                    if field.name in field_names:
+                        raise FieldError('Local field %r in class %r clashes '\
+                                         'with field of similar name from '\
+                                         'abstract base class %r' % \
+                                            (field.name, name, base.__name__))
                     new_class.add_to_class(field.name, copy.deepcopy(field))
 
             # Inherit managers from the abstract base classes.
                 if not val or val is manager:
                     new_manager = manager._copy_to_model(new_class)
                     new_class.add_to_class(mgr_name, new_manager)
+        
+            # Inherit virtual fields (like GenericForeignKey) from the parent class
+            for field in base._meta.virtual_fields:
+                if base._meta.abstract and field.name in field_names:
+                    raise FieldError('Local field %r in class %r clashes '\
+                                     'with field of similar name from '\
+                                     'abstract base class %r' % \
+                                        (field.name, name, base.__name__))
+                new_class.add_to_class(field.name, copy.deepcopy(field))
+        
         if abstract:
             # Abstract base models can't be instantiated and don't appear in
             # the list of models for an app. We do the final setup for them a

django/db/models/options.py

 class Options(object):
     def __init__(self, meta, app_label=None):
         self.local_fields, self.local_many_to_many = [], []
+        self.virtual_fields = []
         self.module_name, self.verbose_name = None, None
         self.verbose_name_plural = None
         self.db_table = ''
         if hasattr(self, '_name_map'):
             del self._name_map
 
+    def add_virtual_field(self, field):
+        self.virtual_fields.append(field)
+
     def setup_pk(self, field):
         if not self.pk and field.primary_key:
             self.pk = field

tests/modeltests/generic_relations/models.py

     def __unicode__(self):
         return self.tag
 
+class ValuableTaggedItem(TaggedItem):
+    value = models.PositiveIntegerField()
+
 class Comparison(models.Model):
     """
     A model that tests having multiple GenericForeignKeys
 >>> Comparison.objects.all()
 [<Comparison: tiger is stronger than None>]
 
+# GenericForeignKey should work with subclasses (see #8309)
+>>> quartz = Mineral.objects.create(name="Quartz", hardness=7)
+>>> valuedtag = ValuableTaggedItem(content_object=quartz, tag="shiny", value=10)
+>>> valuedtag.save()
+>>> valuedtag.content_object
+<Mineral: Quartz>
 
 # GenericInlineFormSet tests ##################################################
 
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.