Friday, December 9, 2011

My BaseModel

When I build projects in Django I like to have a 'core' app with all my common bits in it, including a BaseModel. In that BaseModel I'll define the most basic fields possible, in this case a simple pair of created/modified fields built using custom django-extension fields.

# core/models.py
from django.db import models
from django.utils.translation import ugettext_lazy as _

from core.fields import CreationDateTimeField, ModificationDateTimeField

class BaseModel(models.Model):
    """ Base abstract base class to give creation and modified times """
    created     = CreationDateTimeField(_('created'))
    modified    = ModificationDateTimeField(_('modified'))
    
    class Meta:
        abstract = True

You'll notice I also have core.fields defined. That is because (unless things have changed), django-extensions doesn't work with South out of the box. Hence the file below where I extend those fields to play nicely with my migration tool of choice.

# core/fields.py
from django_extensions.db.fields import CreationDateTimeField, ModificationDateTimeField

class CreationDateTimeField(CreationDateTimeField):

    def south_field_triple(self):
        "Returns a suitable description of this field for South."
        # We'll just introspect ourselves, since we inherit.
        from south.modelsinspector import introspector
        field_class = "django.db.models.fields.DateTimeField"
        args, kwargs = introspector(self)
        return (field_class, args, kwargs)    
        
        
class ModificationDateTimeField(ModificationDateTimeField):

    def south_field_triple(self):
        "Returns a suitable description of this field for South."
        # We'll just introspect ourselves, since we inherit.
        from south.modelsinspector import introspector
        field_class = "django.db.models.fields.DateTimeField"
        args, kwargs = introspector(self)
        return (field_class, args, kwargs)

Unfortunately, this all shows up as red marks when I run coverage.py reports. To deal with that I added in some tests. However, I'll readily I'm not super pleased with the tests below, but they are better then nothing, right?

# core/tests/test_fields.py
from django.test import TestCase

from core.fields import CreationDateTimeField, ModificationDateTimeField

class TestFields(TestCase):
    
    def test_create_override(self):
        field = CreationDateTimeField()
        triple = field.south_field_triple()
        
        self.assertEquals(triple[0], 'django.db.models.fields.DateTimeField')
        self.assertEquals(triple[1], list())
        self.assertEquals(triple[2], {'default': 'datetime.datetime.now', 'blank': 'True'})
        
    def test_modify_override(self):
        field = ModificationDateTimeField()
        triple = field.south_field_triple()
        
        self.assertEquals(triple[0], 'django.db.models.fields.DateTimeField')
        self.assertEquals(triple[1], list())
        self.assertEquals(triple[2], {'default': 'datetime.datetime.now', 'blank': 'True'})

Closing Thoughts

My pattern is also If I need more stuff in this BaseModel I extend it with another abstract class instead of changing it. That way I can be sure at least this part works really well and any additions are isolated in another class.

I'll reiterate that I'm not happy with the tests. I'm open to suggestions.

I pretty much got the BaseModel from Frank Wiles of RevSys back in the summer of 2010. What I added was sticking all the common bits into the core app, getting the South migration to play more nicely, and adding tests.

But much of this is moot!

Note: I added this segment several days after my original posting because of the stuff in the comments. Thanks Jannis Leidel and someone named John - this is part of why I post.

Jannis and John both pointed out that django_extensions now has a TimeStampedModel that does what my BaseModel does. They also pointed out that django_extensions comes with built-in South migrations for it's CreationDateTimeField and ModificationDateTimeField fields.

Which means thanks we can safely just do this and not worry about migrations:

# core/models.py
from django.db import models
from django.utils.translation import ugettext_lazy as _

from django_extensions.db.fields import CreationDateTimeField, ModificationDateTimeField

class BaseModel(models.Model):
    """ Base abstract base class to give creation and modified times """
    created     = CreationDateTimeField(_('created'))
    modified    = ModificationDateTimeField(_('modified'))
    
    class Meta:
        abstract = True

5 comments:

Sean O'Connor said...

Carl's django-model-utils provides similar auto fields and base models that play nice with south. Might be worth checking out :)

John said...

I don't think you need your BaseModel abstract class at all as you don't need to add `created` and `modified` manually, just subclass the TimeStampedModel in django_extensions. I do this and don't need to do anything special to use south.
See http://packages.python.org/django-extensions/model_extensions.html

from django_extensions.db.models import TimeStampedModel
class MyModel(TimeStampedModel):
# model now has 'created' and 'modified' fields
pass

pydanny said...

@John - I think this got added to django_extensions after I created my BaseModel class. But it's a wonderful development and I'm happy!

Diederik van der Boor said...

Awesome idea to have the creationdate and modificationdate as base class fields.

In the recent Django versions (1.2?) it is no longer needed to have separate fields. You could also use:

created = DateTimeField(_('created'), auto_now_add=True)
modified = DateTimeField(_('modified'), auto_now=True)

Making the code even easier :-)

pydanny said...

Diederik van der Boor,

auto_add_now and auto_add are deprecated. We aren't supposed to use them anymore. :D