Django How To Call A Method From A Custom Field Given A Model Instance?
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 Temperature
s, 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?"