diff --git a/django/db/models/base.py b/django/db/models/base.py index 139fc3d8df..9a4d6664ab 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -35,14 +35,29 @@ class ModelBase(type): # Create the class. module = attrs.pop('__module__') - meta = attrs.pop('Meta', None) new_class = type.__new__(cls, name, bases, {'__module__': module}) + attr_meta = attrs.pop('Meta', None) + abstract = getattr(attr_meta, 'abstract', False) + if not attr_meta: + meta = getattr(new_class, 'Meta', None) + else: + meta = attr_meta + base_meta = getattr(new_class, '_meta', None) + new_class.add_to_class('_meta', Options(meta)) - new_class.add_to_class('DoesNotExist', - subclass_exception('DoesNotExist', ObjectDoesNotExist, module)) - new_class.add_to_class('MultipleObjectsReturned', - subclass_exception('MultipleObjectsReturned', - MultipleObjectsReturned, module)) + if not abstract: + new_class.add_to_class('DoesNotExist', + subclass_exception('DoesNotExist', ObjectDoesNotExist, module)) + new_class.add_to_class('MultipleObjectsReturned', + subclass_exception('MultipleObjectsReturned', MultipleObjectsReturned, module)) + if base_meta and not base_meta.abstract: + # Non-abstract child classes inherit some attributes from their + # non-abstract parent (unless an ABC comes before it in the + # method resolution order). + if not hasattr(meta, 'ordering'): + new_class._meta.ordering = base_meta.ordering + if not hasattr(meta, 'get_latest_by'): + new_class._meta.get_latest_by = base_meta.get_latest_by # Do the appropriate setup for any model parents. abstract_parents = [] @@ -86,10 +101,12 @@ class ModelBase(type): % (field.name, name, parent.__name__)) new_class.add_to_class(field.name, field) - if new_class._meta.abstract: + 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 # little differently from normal models. + attr_meta.abstract = False + new_class.Meta = attr_meta return new_class new_class._prepare() diff --git a/django/db/models/options.py b/django/db/models/options.py index 05b1d52f8d..a5e853de33 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -55,7 +55,7 @@ class Options(object): # Next, apply any overridden values from 'class Meta'. if self.meta: - meta_attrs = self.meta.__dict__ + meta_attrs = self.meta.__dict__.copy() del meta_attrs['__module__'] del meta_attrs['__doc__'] for attr_name in DEFAULT_NAMES: diff --git a/docs/model-api.txt b/docs/model-api.txt index 6ca08ae4e5..2687c00d14 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -2031,6 +2031,18 @@ You can also prevent saving:: Model inheritance ================= +**New in Django development version** + +Model inheritance in Django works almost identically to the way normal class +inheritance works in Python. The only decision you have to make is whether you +want the parent models to be models in their own right (with their own +database tables), or if the parents are just holders of common information +that will only be visible through the child models. + +Often, you will just want to use the parent class to hold information that you +don't want to have to type out for each child model. This class isn't going to +ever be used in isolation, so `abstract base classes`_ are what you're after. However, if you're subclassing an existing model (perhaps something from another application entirely), or want each model to have its own database table, `multi-table inheritance`_ is the way to go. + Abstract base classes --------------------- @@ -2063,6 +2075,38 @@ For many uses, this type of model inheritance will be exactly what you want. It provides a way to factor out common information at the Python level, whilst still only creating one database table per child model at the database level. +``Meta`` inheritance +~~~~~~~~~~~~~~~~~~~~ + +When an abstract base class is created, Django makes any ``Meta`` inner class +you declared on the base class available as an attribute. If a child class +does not declared its own ``Meta`` class, it will inherit the parent's +``Meta``. If the child wants to extend the parent's ``Meta`` class, it can +subclass it. For example:: + + class CommonInfo(models.Model): + ... + class Meta: + abstract = True + ordering = ['name'] + + class Student(CommonInfo): + ... + class Meta(CommonInfo.Meta): + db_table = 'student_info' + +Django does make one adjustment to the ``Meta`` class of an abstract base +class: before installing the ``Meta`` attribute, it sets ``abstract=False``. +This means that children of abstract base classes don't automatically become +abstract classes themselves. Of course, you can make an abstract base class +that inherits from another abstract base class. You just need to remember to +explicitly set ``abstract=True`` each time. + +Some attributes won't make sense to include in the ``Meta`` class of an +abstract base class. For example, including ``db_table`` would mean that all +the child classes (the ones that don't specify their own ``Meta``) would use +the same database table, which is almost certainly not what you want. + Multi-table inheritance ----------------------- @@ -2100,8 +2144,29 @@ However, if ``p`` in the above example was *not* a ``Restaurant`` (it had been created directly as a ``Place`` object or was the parent of some other class), referring to ``p.restaurant`` would give an error. -Normally you won't need to worry too much about how model inheritance works. -It will behave similarly to Python class inheritance. +``Meta`` and multi-table inheritance +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the multi-table inheritance situation, it doesn't make sense for a child +class to inherit from its parent's ``Meta`` class. All the ``Meta`` options +have already been applied to the parent class and applying them again would +normally only lead to contradictory behaviour (this is in contrast with the +abstract base class case, where the base class doesn't exist in its own +right). + +So a child model does not have access to its parent's ``Meta`` class. However, +there are a few limited cases where the child inherits behaviour from the +parent: if the child does not specify an ``ordering`` attribute or a +``get_latest_by`` attribute, it will inherit these from its parent. + +If the parent has an ordering and you don't want the child to have any natural +ordering, you can explicity set it to be empty:: + + class ChildModel(ParentModel): + ... + class Meta: + # Remove parent's ordering effect + ordering = [] Inheritance and reverse relations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -2127,6 +2192,23 @@ For more information about reverse relations, refer to the `Database API reference`_ . For now, just remember to run ``manage.py validate`` when you're writing your models and pay attention to the error messages. +Multiple inheritance +-------------------- + +Just as with Python's subclassing, it's possible for a Django model to inherit +from multiple parent models. Keep in mind that normal Python name resolution +rules apply. The first base class that a particular name appears in (e.g. +``Meta``) will be the one that is used. We stop searching once we find the +name once. This means that if multiple parents contain a ``Meta`` class, only +the first one is going to be used. All others will be ignored. + +Generally, you won't need to inherit from multiple parents. The main use-case +where this is useful is for ''mix-in'' classes: adding a particular extra +field or method to every class that inherits the mix-in. Try to keep your +inheritance hierarchies as simple and straightforward as possible so that you +won't have to struggle to work out where a particular piece of information is +coming from. + Models across files =================== diff --git a/tests/modeltests/model_inheritance/models.py b/tests/modeltests/model_inheritance/models.py index 940b2ee10c..1187c5fe64 100644 --- a/tests/modeltests/model_inheritance/models.py +++ b/tests/modeltests/model_inheritance/models.py @@ -20,6 +20,7 @@ class CommonInfo(models.Model): class Meta: abstract = True + ordering = ['name'] def __unicode__(self): return u'%s %s' % (self.__class__.__name__, self.name) @@ -30,6 +31,9 @@ class Worker(CommonInfo): class Student(CommonInfo): school_class = models.CharField(max_length=10) + class Meta: + pass + class Place(models.Model): name = models.CharField(max_length=50) address = models.CharField(max_length=80) @@ -37,7 +41,13 @@ class Place(models.Model): def __unicode__(self): return u"%s the place" % self.name -class Restaurant(Place): +class Rating(models.Model): + rating = models.IntegerField(null=True, blank=True) + + class Meta: + abstract = True + +class Restaurant(Place, Rating): serves_hot_dogs = models.BooleanField() serves_pizza = models.BooleanField() @@ -71,6 +81,8 @@ __test__ = {'API_TESTS':""" >>> w = Worker(name='Fred', age=35, job='Quarry worker') >>> w.save() +>>> w2 = Worker(name='Barney', age=34, job='Quarry worker') +>>> w2.save() >>> s = Student(name='Pebbles', age=5, school_class='1B') >>> s.save() >>> unicode(w) @@ -78,6 +90,16 @@ u'Worker Fred' >>> unicode(s) u'Student Pebbles' +# The children inherit the Meta class of their parents (if they don't specify +# their own). +>>> Worker.objects.values('name') +[{'name': u'Barney'}, {'name': u'Fred'}] + +# Since Student does not subclass CommonInfo's Meta, it has the effect of +# completely overriding it. So ordering by name doesn't take place for Students. +>>> Student._meta.ordering +[] + # However, the CommonInfo class cannot be used as a normal model (it doesn't # exist as a model). >>> CommonInfo.objects.all() @@ -96,7 +118,7 @@ AttributeError: type object 'CommonInfo' has no attribute 'objects' >>> p2.save() Test constructor for Restaurant. ->>> r = Restaurant(name='Demon Dogs', address='944 W. Fullerton', serves_hot_dogs=True, serves_pizza=False) +>>> r = Restaurant(name='Demon Dogs', address='944 W. Fullerton',serves_hot_dogs=True, serves_pizza=False, rating=2) >>> r.save() # Test the constructor for ItalianRestaurant. @@ -106,9 +128,9 @@ Test constructor for Restaurant. # Make sure Restaurant and ItalianRestaurant have the right fields in the right # order. >>> [f.name for f in Restaurant._meta.fields] -['id', 'name', 'address', 'place_ptr', 'serves_hot_dogs', 'serves_pizza'] +['id', 'name', 'address', 'place_ptr', 'rating', 'serves_hot_dogs', 'serves_pizza'] >>> [f.name for f in ItalianRestaurant._meta.fields] -['id', 'name', 'address', 'place_ptr', 'serves_hot_dogs', 'serves_pizza', 'restaurant_ptr', 'serves_gnocchi'] +['id', 'name', 'address', 'place_ptr', 'rating', 'serves_hot_dogs', 'serves_pizza', 'restaurant_ptr', 'serves_gnocchi'] # Even though p.supplier for a Place 'p' (a parent of a Supplier), a Restaurant # object cannot access that reverse relation, since it's not part of the @@ -118,7 +140,7 @@ Test constructor for Restaurant. >>> Restaurant.objects.filter(supplier__name='foo') Traceback (most recent call last): ... -TypeError: Cannot resolve keyword 'supplier' into field. Choices are: address, id, italianrestaurant, lot, name, place_ptr, provider, serves_hot_dogs, serves_pizza +TypeError: Cannot resolve keyword 'supplier' into field. Choices are: address, id, italianrestaurant, lot, name, place_ptr, provider, rating, serves_hot_dogs, serves_pizza # Parent fields can be used directly in filters on the child model. >>> Restaurant.objects.filter(name='Demon Dogs')