Skip to content Skip to sidebar Skip to footer

Django How To Call A Method From A Custom Field Given A Model Instance?

I have the following model: class CustomField(models.CharField): def foo(self): return 'foo' class Test(models.Model): col1 = models.CharField(max_length=45) c

Solution 1:

There is a saying in computer science by David Wheeler that "All problems in computer science can be solved by another level of indirection (except too many layers of indirection)".

We thus can define a class Temperature for example to store the temperature:

from enum import Enum
from decimal import Decimal
NINE_FIFTHS = Decimal(9)/Decimal(5)

classTemperatureUnit(Enum):
    KELVIN = (1,0, 'K')
    FAHRENHEIT = (NINE_FIFTHS, Decimal('-459.67'), '°F')
    CELSIUS = (1, Decimal('-273.15'), '°C')
    RANKINE = (NINE_FIFTHS, 0, '°R')

classTemperature:

    def__init__(self, kelvin, unit=TemperatureUnit.CELSIUS):
        self.kelvin = Decimal(kelvin)
        self.unit = unit

    @staticmethoddeffrom_unit(value, unit=TemperatureUnit.CELSIUS):
        a, b, *__ = unit.value
        return Temperature((value-b)/a, unit)

    @propertydefvalue(self):
        a, b, *__ = self.unit.value
        return a * self.kelvin + b

    defconvert(self, unit):
        return Temperature(self.kelvin, unit)

    def__str__(self):
        return'{} {}'.format(self.value, self.unit.value[2])

For example we can here create tempratures:

>>> str(Temperature(15, unit=TemperatureUnit.FAHRENHEIT))
'-432.67 °F'>>> str(Temperature(0, unit=TemperatureUnit.FAHRENHEIT))
'-459.67 °F'>>> str(Temperature(1, unit=TemperatureUnit.FAHRENHEIT))
'-457.87 °F'>>> str(Temperature(0, unit=TemperatureUnit.FAHRENHEIT))
'-459.67 °F'>>> str(Temperature(0, unit=TemperatureUnit.CELSIUS))
'-273.15 °C'

Now we can make a Django model field that stores and retrieves Temperatures, by saving these for example in a decimal on the database side, in Kelvin:

classTemperatureField(models.DecimalField):

    deffrom_db_value(self, value):
        kelvin = super().from_db_value(value)
        if kelvin isnotNone:
            return Temperature(kelvin)
        returnNonedefto_python(self, value):
        ifisinstance(value, Temperature):
            return value
        if value isNone:
            return value
        kelvin = super().to_python(value)
        return Temperature(kelvin)

    defget_prep_value(self, value):
        ifisinstance(value, Temperature):
            value = value.kelvin
        returnsuper().get_prep_value(value)

The above is of course a raw sketch. See the documentation on writing custom model fields for more information. You can add a form field, widget, lookups to query the database, etc. So you can define an extra layer of logic to your TemperatureField.

Solution 2:

Here is a slightly modified, working version of WillemVanOnsem's wonderful answer:

classTemperatureField(models.DecimalField):

    deffrom_db_value(self, value, expression, connection):
        if value isnotNone:
            return Temperature(value)
        returnNonedefto_python(self, value):
        ifisinstance(value, Temperature):
            return value
        if value isNone:
            return value
        kelvin = super().to_python(value)
        return Temperature(kelvin)

    defget_prep_value(self, value):
        ifisinstance(value, Temperature):
            value = value.kelvin
        returnsuper().get_prep_value(value)

    defget_db_prep_save(self, value, connection):
        ifisinstance(value, Temperature):
            return connection.ops.adapt_decimalfield_value(value.kelvin, self.max_digits, self.decimal_places)
        elifisinstance(value, (float, int)):
            return connection.ops.adapt_decimalfield_value(Decimal(value), self.max_digits, self.decimal_places)
        elifisinstance(value, (Decimal,)):
            return connection.ops.adapt_decimalfield_value(Decimal(value), self.max_digits, self.decimal_places)

Test(models.Model):
    temp = TemperatureField(max_digits=10, decimal_places=2)

A few notes:

In order to save custom field types to your DB, you have to override get_db_prep_value, so that your model knows how to handle Temperature objects, otherwise, your model will think it's working with a Decimal, which will result in:

AttributeError: 'Temperature' object has no attribute 'quantize'

Clear error with an easy fix...

Now, the docs on from_db_value:

If present for the field subclass, from_db_value() will be called in all circumstances when the data is loaded from the database, including in aggregates and values() calls.

emphasis on when the data is loaded from the database!

This means that when you call t = Test.objects.create(...), from_db_value will not be evaluated, and the corresponding custom column for the t instance will be equal to whatever value you set it to in the create statement!

For example:

>>>t = Test.objects.create(temp=1)
>>>t.temp
1
>>>type(t.temp)
<class'int'>

>>>t = Test.objects.first()
>>>t.temp
<extra_fields.fields.Temperature object at 0x10e733e50>
>>> type(t.temp)
<class'extra_fields.fields.Temperature'>

If you tried to run the original version of from_db_value:

deffrom_db_value(self, value):
    kelvin = super().from_db_value(value)
    if kelvin isnotNone:
        return Temperature(kelvin)
    returnNone

You won't even get errors until you call:

>>>t = Test.objects.get(...)
TypeError: from_db_value() takes 2 positional arguments but 4 were given
AttributeError: 'super'object has no attribute 'from_db_value'

Lastly, note that from_db_value is not a method in any of Django's base model fields, so calling super().from_db_value will always throw an error. Instead the Field base class will check for the existence of from_db_value:

defget_db_converters(self, connection):
    ifhasattr(self, 'from_db_value'):
        return [self.from_db_value]
    return []

Post a Comment for "Django How To Call A Method From A Custom Field Given A Model Instance?"