简体   繁体   中英

How to make a Django model fields calculated at runtime?

I have a model:

class Person (models.Model):
    name     = models.CharField ()
    birthday = models.DateField ()
    age      = models.IntegerField ()

I want to make age field to behave like a property:

    def get_age (self):
        return (datetime.datetime.now() - self.birthday).days // 365

    age = property (get_age)

but at the same time I need age to be a true field, so I can find it in Person._meta.fields , and assign attributes to it: age.help_text = "Age of the person" , etc.

Obviously I cannot just override Person.save() method to calculate and store age in the database, because it inevitably will become wrong later (in fact, it shouldn't be stored in the database at all).

Actually, I don't need to have setters now, but a nice solution must have setting feature.

Is it possible in Django, or probably there is a more pythonic and djangoic approach to my problem?

you're going about it in a strange way. you only need to store the birthday (which you're already doing).

if you need to do queries based on age, take in the age, and then search on the birthdays that fit the requirements.

from datetime import datetime

# get people over 50
age = 50 # in years
age_ago = datetime.now() - age # use timedelta i don't know the syntax off the top of my head
Person.objects.filter(birthday__lte=age_ago) # people whose birthday is before fifty years ago

you said it yourself "[age] shouldn't be stored in the database at all"

you're right. there is no reason to have the age field... just the property will be fine.

Another way to go about it which might be simpler, is to use custom form in the admin model. Something like this:

class Person (models.Model):
    birthday = models.DateField()

class PersonForm(forms.ModelForm):
    age = forms.IntegerField() # This will not be in the database
    class Meta:
        model = Person
    def __init__(self, *args, **kwargs):
        # verify instance was passed
        instance = kwargs.get('instance')
        if instance:
            self.base_fields['age'].initial = (datetime.datetime.now() - self.birthday).days
        super(PersonForm, self).__init__(*args, **kwargs)

class FooAdmin(admin.ModelAdmin):
    form = PersonForm
    # Save the age in the birthday field
    def save_model(self, request, obj, form, change):
       obj.birthday = datetime.datetime.now() + form.cleaned_data['age']
       obj.save()

I think the solution is to create a custom widget for the birthday field. All you want is to change how the birthday is displayed and how it is edited. that is exactly the widget purpose.

Sorry I don't have the code for that, but here is a good place to start:

https://docs.djangoproject.com/en/dev/ref/forms/widgets/

You can achieve this by using django-computed-property .

You first have to pip install django-computed-property with the following command:

pip install django-computed-property

You then add computed_property to your list of INSTALLED_APPS in settings.py :

INSTALLED_APPS = [
    ...
    'computed_property'
]

Now you can import and use the included field classes in your models like so:

from django.db import models
from computed_property import ComputedTextField

import random


class Person(models.Model):
    age = ComputedTextField(compute_from='calculation')

    @property
    def calculation(self):
        return str(random.randint(1,101))

The age field on the Person model returns a random number each time it is called to demonstrate that it is calculated at runtime.

You can read values from the age field as usual, but you may not set the field's value. When the field is accessed and when a model instance is saved, it will compute the field's value using the provided callable (function/lambda), property age , or attribute age

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM