I'm writing a lot of models these time and most of them requires some generic informations, some kind of meta data like who create the object and when, who was the last to modify it and when.
I used to add manually those fields to my models, but even when they represented the same data I used different names and it started to bug me. For example, if the model was for storing messages I would probably use sent_by and when I'm dealing with something else, like a calendar event for example, I would most likely use created_by. It quickly becomes boring trying to remember how I named those field for each models, especially when semantically they are equivalents.
Then I recalled that Django's models support inheritance, which is awesome since it would allow me to reuse those fields and thus normalizing my field names. How pretty.
So now instead of;
from django.db import models
class Item(models.Model):
label = models.CharField(max_length=255, unique=True)
created_by = models.ForeignKey(User, blank=True, related_name='object_creator')
created_time = models.DateTimeField(auto_now_add=True)
modified_by = models.ForeignKey(User, blank=True, related_name='object_modifier')
modified_time = models.DateTimeField(auto_now=True)
I simply do;
from django.db import models
from myproject.myapp.models import ExtendedModel
class Item(ExtendedModel):
label = models.CharField(max_length=255, unique=True)
My model file is literally cut in half, me really happy.
The ExtendedModel looks like this;
class ExtendedModel(models.Model):
created_by = models.ForeignKey(User, blank=True, related_name='%(class)s_creator')
created_time = models.DateTimeField(auto_now_add=True)
modified_by = models.ForeignKey(User, blank=True, related_name='%(class)s_modifier')
modified_time = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
UPDATE: I added abstract = True because someone pointed out I should use it. He didn't say why but after some digging I found that it makes sense. If I don't use an abstract base class Django will create two tables and keep the relationship using a table join, which is not really bad but not really optimal either. By specifying abstract Django knows it must create only one table and thus avoid table joins.
UPDATE 2: I also replaced 'object_modifier' and 'object_creator' with '%(class)s_modifier' and '%(class)s_creator'. This way related names are created dynamically so they don't clash when multiple models are used.
I knew for quite a while about model inheritance in Django so I can't really explain why I didn't do this before, sometime it's just too obvious I guess. However there's one little bit of magic to add in order to make it silky smooth and transparent with the Django admin.
The main problem is that we want those field to auto-populate themselves. It's piece of cake for the DateTime field using auto_now*, but it's not that simple with the user fields. By design Django doesn't allow access to the request object within the model so it's not really possible to get the user who performed the request and bypassing this design decision somewhat defeat a good design decision.
We could overload the save method with a custom form, but it would only do the trick for custom views. The Django admin interface would still ask you to fill out manually those fields, which is not really optimal and really easy to cheat or at least, not really reliable.
To achieve what we want, we need to overload the save method in an admin object. Fortunately admin objects also support inheritance, so we can do it quite easily without repeating ourselves;
from myproject.myapp.models import Item
from django.contrib import admin
admin_site = admin.AdminSite()
class ExtendedModelAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
instance = form.save(commit=False)
if not hasattr(instance, 'created_by'):
instance.created_by = request.user
instance.modified_by = request.user
instance.save()
form.save_m2m()
return instance
def save_formset(self, request, form, formset, change):
def set_user(instance):
if not hasattr(instance, 'created_by'):
instance.created_by = request.user
instance.modified_by = request.user
instance.save()
if formset.model == User:
instances = formset.save(commit=False)
map(set_user, instances)
formset.save_m2m()
return instances
else:
return formset.save()
class ItemAdmin(ExtendedModelAdmin):
list_display=['label', 'modified_time', 'modified_by', 'created_time', 'created_by']
ordering = ['-created_time']
fields = ('label')
admin_site.register(Item, ItemAdmin)
That's it !
I have used abstract model for inheritance before, it is a good idea to have a model that has the basic who and when information to use on with other models.
Thanks.
thanks for this excellent tip and writeup. i made one small addition to your ModelAdmin class so that the created_by and modified_by fields don't show up on the change form.
...and i was trying to put the code into that comment above using the "code highlight" documentation, but i guess i couldn't get the preview working right and then when i clicked preview a second time on the preview page, it posted!
so anyway, all i did to exclude those fields was add the following line
exclude = ('created_by','modified_by',)right after the Class definition line and before the first method definition