83

I have a simple Employee model that includes firstname, lastname and middlename fields.

On the admin side and likely elsewhere, I would like to display that as:

lastname, firstname middlename

To me the logical place to do this is in the model by creating a calculated field as such:

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)
    name = ''.join(
        [lastname.value_to_string(),
        ',',
         firstname.value_to_string(),
        ' ',
         middlename.value_to_string()])

    class Meta:
        ordering = ['lastname','firstname', 'middlename']

class EmployeeAdmin(admin.ModelAdmin):
    list_display = ('clocknumber','name')
    fieldsets = [("Name", {"fields":(("lastname", "firstname", "middlename"), "clocknumber")}),
        ]

admin.site.register(Employee, EmployeeAdmin)

Ultimately what I think I need is to get the value of the name fields as strings. The error I am getting is value_to_string() takes exactly 2 arguments (1 given). Value to string wants self, obj. I am not sure what obj means.

There must be an easy way to do this, I am sure I am not the first to want to do this.

Edit: Below is my code modified to Daniel's answer. The error I get is:

django.core.exceptions.ImproperlyConfigured: 
    EmployeeAdmin.list_display[1], 'name' is not a callable or an 
    attribute of 'EmployeeAdmin' of found in the model 'Employee'.
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)

    @property
    def name(self):
        return ''.join(
            [self.lastname,' ,', self.firstname, ' ', self.middlename])

    class Meta:
        ordering = ['lastname','firstname', 'middlename']

class EmployeeAdmin(admin.ModelAdmin):
    list_display = ('clocknumber','name')
    fieldsets = [("Name", {"fields":(("lastname", "firstname", "middlename"), "clocknumber")}),
]

admin.site.register(Employee, EmployeeAdmin)
rmcsharry
  • 5,363
  • 6
  • 65
  • 108
cstrutton
  • 5,667
  • 3
  • 25
  • 32
  • 1
    possible duplicate of [Django Model: field value is calculation of other fileds](http://stackoverflow.com/questions/11465293/django-model-field-value-is-calculation-of-other-fileds) – Anto Mar 11 '14 at 16:16

5 Answers5

100

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.

Daniel Roseman
  • 588,541
  • 66
  • 880
  • 895
  • I assumed this is placed in the Employee model... that gave me an error. Should that method go into the EmployeeAdmin class? – cstrutton Jul 16 '13 at 17:09
  • I edited the question to show the code I used and the error message I got. BTW Django 1.5.1 and python 2.7 if that matters. – cstrutton Jul 16 '13 at 17:39
  • Can this @property be used in a Foreign key for composite primary keys? – CpILL Feb 10 '14 at 03:10
  • @CpILL: No, because it is handled only at Python side, while foreign keys are handled also at the database. – Denilson Sá Maia Aug 27 '14 at 21:42
  • There are issues with this approach, depending on use-case. Derived fields (using annotations for instance) would be a nice addition to Django. That being said, for simple concatenations, this should be fine in most cases. – DylanYoung Jan 30 '17 at 21:34
  • 3
    This does not allow you to filter() a QuerySet because in order to do so it must be a Field and not a property. What worked for me was to annotate on an expression (using QuerySet.annotate() ) that tells the database how to derive the value (e.g. models.F(field1) - models.F(field2), and then filter on that annotation instead. – ThatOneDude Mar 08 '19 at 02:36
68

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.

Community
  • 1
  • 1
Alex Petralia
  • 1,730
  • 1
  • 22
  • 39
  • Just for my own understanding, why isn't def __str__(self): return self.whatever an adequate solution to this problem? This is mentioned in the docs right under the part the OP referenced (Django 1.10 version) https://docs.djangoproject.com/en/1.10/topics/db/models/#model-methods. Is this so that you can have more than one way of calling or naming an object? Alternatively, can you do something like def__str2__ return self.othername? – Malik A. Rumi Mar 20 '17 at 13:34
  • @MalikA.Rumi `self.whatever` will only apply to a single instance of the model, but will not apply to QuerySets (that is, all the instances returned by `.all()` or `.filter()`). `self.whatever` refers to a single instance and would not exist for QuerySets. – Alex Petralia Mar 29 '17 at 14:35
  • Thanks Alex, this works for me with Django 1.11. Also I needed to use these annotated fields in the Admin but wasn't possible, but a workaround [here](https://stackoverflow.com/a/26618982/503743) allows to use this also with the Admin interfaces. – Mariano Ruiz Aug 04 '17 at 13:57
  • This is the most complicated but most efficient solution. Using Python's native property decorator would drastically drop down the performance in case the property needs to do database queries. – Dingkun Liu Feb 25 '20 at 17:18
56

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)
mojomex
  • 139
  • 1
  • 12
cstrutton
  • 5,667
  • 3
  • 25
  • 32
15

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')
brechin
  • 569
  • 4
  • 7
  • I've tried this app. I first though it avoid creation of a field in the DB but it doesn't. Moreover, after implementation, migration and loop through the model (cfr [doc note](https://django-computed-property.readthedocs.io/en/latest/)) to update the computed field => computed results weren't visible in the DB! And even if it was working what is the advantage of using this app as compare to using the save() method in the model?! – openHBP Oct 17 '19 at 13:22
3

In this case if you are only going to use the field for representation in admin site and such issues, you might better to consider overriding str() or unicode() method of the class as it is mentioned in django documentation here:

class Employee(models.Model):
    # fields definitions
    def __str__(self):
        return self.lastname + ' ,' + self.firstname + ' ' + self.middlename
Farzad Vertigo
  • 2,458
  • 1
  • 29
  • 32