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')