How to add a calculated field to a Django model
Solution 1:
That's not something you do as a field. Even if that syntax worked, it would only give the value when the class was defined, not at the time you access it. You should do this as a method, and you can use the @property
decorator to make it look like a normal attribute.
@property
def name(self):
return ''.join(
[self.lastname,' ,', self.firstname, ' ', self.middlename])
self.lastname
etc appear as just their values, so no need to call any other method to convert them.
Solution 2:
Daniel Roseman's solution makes a calculated field an attribute of a Model
, however it does not make it accessible via QuerySet methods (eg. .all()
, .values()
). This is because QuerySet methods call the database directly, circumventing the django Model
.
Since QuerySets access the database directly, the solution is to override the Manager
's .get_queryset()
method by appending your calculated field. The calculated field is created using .annotate()
. Finally, you set the objects
Manager in your Model
to your new Manager
.
Here is some code demonstrating this:
models.py
from django.db.models.functions import Value, Concat
from django.db import Model
class InvoiceManager(models.Manager):
"""QuerySet manager for Invoice class to add non-database fields.
A @property in the model cannot be used because QuerySets (eg. return
value from .all()) are directly tied to the database Fields -
this does not include @property attributes."""
def get_queryset(self):
"""Overrides the models.Manager method"""
qs = super(InvoiceManager, self).get_queryset().annotate(link=Concat(Value("<a href='#'>"), 'id', Value('</a>')))
return qs
class Invoice(models.Model):
# fields
# Overridden objects manager
objects = InvoiceManager()
Now, you will be able to call .values()
or .all()
and access the newly calculated link
attribute as declared in the Manager
.
It would have also been possible to use other functions in .annotate()
, such as F()
.
I believe the attribute would still not be available in object._meta.get_fields()
. I believe you can add it here, but I haven't explored how - any edits/comments would be helpful.
Solution 3:
Ok... Daniel Roseman's answer seemed like it should have worked. As is always the case, you find what you're looking for after you post the question.
From the Django 1.5 docs I found this example that worked right out of the box. Thanks to all for your help.
Here is the code that worked:
from django.db import models
from django.contrib import admin
class Employee(models.Model):
lastname = models.CharField("Last", max_length=64)
firstname = models.CharField("First", max_length=64)
middlename = models.CharField("Middle", max_length=64)
clocknumber = models.CharField(max_length=16)
def _get_full_name(self):
"Returns the person's full name."
return '%s, %s %s' % (self.lastname, self.firstname, self.middlename)
full_name = property(_get_full_name)
class Meta:
ordering = ['lastname','firstname', 'middlename']
class EmployeeAdmin(admin.ModelAdmin):
list_display = ('clocknumber','full_name')
fieldsets = [("Name", {"fields":(("lastname", "firstname", "middlename"), "clocknumber")}),
]
admin.site.register(Employee, EmployeeAdmin)
Solution 4:
I recently worked on a library that may solve the problem you're having quite easily.
https://github.com/brechin/django-computed-property
Install that, add to INSTALLED_APPS and then
class Employee(models.Model):
...
name = computed_property.ComputedCharField(max_length=3 * 64, compute_from='full_name')
@property
def full_name(self):
return '{LAST}, {FIRST} {MIDDLE}'.format(LAST=self.lastname, FIRST=self.firstname, MIDDLE=self.middlename')