One of the first issues I ran into when I started with Django was the lack of support for model inheritance. Not true model inheritance on the SQL level, but just the possibility to inherit from other models in Python to reuse field definitions (i.e. copy them). Finally, I came up with this snippet, which exploited the fact that models can actually have non-model bases, and just added the shared fields to a model in a separate function call.
jmrbcu eventually posted a much better version that I’ve been using every since. There were still some problems with it though:
- It didn’t work with new-style base classes.
- The fields were not inserted at the position of the expander field, but always either at the beginning or the end of the model.
- I still wasn’t 100% happy with the fake expander fields that were required, especially since you had to use different names if you wanted to do multiple expansions (common1, common2, …). You also had to repeat the class name of the mixin, once as a base class for the model, and again when calling the expander field.
The following code attempts to fix all of those:
import types from copy import deepcopy from django.db import models from django.db.models.fields import Field from django.db.models.base import ModelBase class ExpanderField(object): def __init__(self, *field_container_classes): self.field_container_classes = field_container_classes def contribute_to_class(self, cls, name): for container in self.field_container_classes: for name, value in clone_class(container): cls.add_to_class(name, value) def use_bases(*classes): class ExpandableModelBase(ModelBase): def __new__(cls, name, bases, attrs): bases = list(bases) for klass in reversed(classes): # Add as a base class to allow method overriding. MRO # requires us to list the mixins *before* models.Model. bases.insert(0, klass) # Insert copies of all attributes for attr_name, attr_value in clone_class(klass): # Make sure we do not hide any existing attributes: The # mixins are supposed to act as a base classes after all. # TODO: This currently works not as expected if multiple # mixins define the same name, because of reversed(). if not attr_name in attrs: attrs.update({attr_name: attr_value}) return ModelBase.__new__(cls, name, tuple(bases), attrs) return ExpandableModelBase def clone_class(klass): for attr in [attr for attr in dir(klass) if not attr.startswith('__')]: clone = None attr_value = getattr(klass, attr) if type(attr_value) != types.MethodType: clone = deepcopy(attr_value) # to have the fields in the right order, hack creation_counter. if isinstance(clone, Field): clone.creation_counter = Field.creation_counter Field.creation_counter += 1 # or create throwaway Field instance? # avoid ``related_name`` clashes if (isinstance(clone, models.ForeignKey) or isinstance(clone, models.ManyToManyField)) and clone.rel.related_name is not None: clone.rel.related_name = cls.__name__ + '_' + clone.rel.related_name if clone is not None: yield attr, clone
If you care about field ordering, you can still expand fields manually, as in jmrbcu original snippet:
class CommonFields(object): created_by = models.ForeignKey(User) last_modified_by = models.ForeignKey(User) def save(self): # ... return super(CommonFields, self).save() class NewsItem(CommonFields, models.Model): title = models.CharField() body = models.CharField() common = ExpanderField(CommonFields)
If you are ok with the fields being added at the end of the model, you can do:
class NewsItem(models.Model): __metaclass__ = use_bases(CommonFields) title = models.CharField() body = models.CharField()
For completeness sake, srid has taken this one step further, avoiding the __metaclass__ assignment by hacking the stack, although I think that is a but too much for my taste.
In case you are wondering what this is useful for, here are two examples of code that I currently have in use:
class Timestamped(object): created_at = models.DateTimeField(editable=False) updated_at = models.DateTimeField(editable=False) created_by = models.ForeignKey(User, editable=False, blank=True, null=True) def save(self): now = datetime.datetime.now() if not self.id: self.created_at = now self.updated_at = now super(Timestamped, self).save() class Publication(models.Model): __metaclass__ = use_bases(Timestamped) # ... class Issue(models.Model): __metaclass__ = use_bases(Timestamped) # ...
This is simple. Every model using this mixin will automatically keep track of when objects are created and changed. It also adds the “created_by“ field, but it has to be managed manually. I have this on almost all of my models.
class Approvable(object): is_approved = models.BooleanField(verbose_name="approved", editable=False, default=True) approved_at = models.DateTimeField(editable=False, blank=True, null=True) def __setattr__(self, name, value): # ignore the first calls when Field instances are still being # replaced by their plain values. if not isinstance(self.is_approved, models.Field): # if we are set to "approved", remember to update the timestamp if name == 'is_approved' and value != self.is_approved and value: self._date_needs_update = True return super(Approvable, self).__setattr__(name, value) def save(self): if self.is_approved: # for new items, always update if (not self.id): update_approved_date = True # for existing items, check if the value changed else: update_approved_date = getattr(self, '_date_needs_update', False) self._date_needs_update = False if update_approved_date: self.approved_at = datetime.datetime.now() super(Approvable, self).save() class Review(models.Model): __metaclass__ = use_bases(Timestamped, Approvable) # ...
I have this applied to numerous models that require confirmation by an editor or administrator, like comments and generally stuff from users with limited rights.