Dynamically add properties to a django model

6k views Asked by At

I have a Django model where a lot of fields are choices. So I had to write a lot of "is_something" properties of the class to check whether the instance value is equal to some choice value. Something along the lines of:

class MyModel(models.Model):
    some_choicefield = models.IntegerField(choices=SOME_CHOICES)

    @property
    def is_some_value(self):
        return self.some_choicefield == SOME_CHOICES.SOME_CHOICE_VALUE

    # a lot of these...

In order to automate this and spare me a lot of redundant code, I thought about patching the instance at creation, with a function that adds a bunch of methods that do the checks. The code became as follows (I'm assuming there's a "normalize" function that makes the label of the choice a usable function name):

def dynamic_add_checks(instance, field):
    if hasattr(field, 'choices'):
        choices = getattr(field, 'choices')
        for (value,label) in choices:
            def fun(instance):
                return getattr(instance, field.name) == value
            normalized_func_name = "is_%s_%s" % (field.name, normalize(label))
            setattr(instance, normalized_func_name, fun(instance))

class MyModel(models.Model):
    def __init__(self, *args, **kwargs):
        super(MyModel).__init__(*args, **kwargs)
        dynamic_add_checks(self, self._meta.get_field('some_choicefield')

    some_choicefield = models.IntegerField(choices=SOME_CHOICES)

Now, this works but I have the feeling there is a better way to do it. Perhaps at class creation time (with metaclasses or in the new method)? Do you have any thoughts/suggestions about that?

3

There are 3 answers

4
Wtower On

Well I am not sure how to do this in your way, but in such cases I think the way to go is to simply create a new model, where you keep your choices, and change the field to ForeignKey. This is simpler to code and manage.

You can find a lot of information at a basic level in Django docs: Models: Relationships. In there, there are many links to follow expanding on various topics. Beyong that, I believe it just needs a bit of imagination, and maybe trial and error in the beginning.

3
hspandher On

I came across a similar problem where I needed to write large number of properties at runtime to provide backward compatibility while changing model fields. There are 2 standard ways to handle this -

  1. First is to use a custom metaclass in your models, which inherits from models default metaclass.
  2. Second, is to use class decorators. Class decorators sometimes provides an easy alternative to metaclasses, unless you have to do something before the creation of class, in which case you have to go with metaclasses.
0
Johnny Zhao On

I bet you know Django fields with choices provided will automatically have a display function.

Say you have a field defined like this:

category = models.SmallIntegerField(choices=CHOICES)

You can simply call a function called get_category_display() to access the display value. Here is the Django source code of this feature:

https://github.com/django/django/blob/baff4dd37dabfef1ff939513fa45124382b57bf8/django/db/models/base.py#L962

https://github.com/django/django/blob/baff4dd37dabfef1ff939513fa45124382b57bf8/django/db/models/fields/init.py#L704

So we can follow this approach to achieve our dynamically set property goal.

Here is my scenario, a little bit different from yours but down to the end it's the same:

I have two classes, Course and Lesson, class Lesson has a ForeignKey field of Course, and I want to add a property name cached_course to class Lesson which will try to get Course from cache first, and fallback to database if cache misses:

Here is a typical solution:

from django.db import models


class Course(models.Model):
    # some fields


class Lesson(models.Model):
    course = models.ForeignKey(Course)

    @property
    def cached_course(self):
        key = key_func()
        course = cache.get(key)
        if not course:
            course = get_model_from_db()
            cache.set(key, course)
        return course

Turns out I have so many ForeignKey fields to cache, so here is the code following the similar approach of Django get_FIELD_display feature:

from django.db import models
from django.utils.functional import curry


class CachedForeignKeyField(models.ForeignKey):

    def contribute_to_class(self, cls, name, **kwargs):
        super(models.ForeignKey, self).contribute_to_class(cls, name, **kwargs)
        setattr(cls, "cached_%s" % self.name,
                property(curry(cls._cached_FIELD, field=self)))


class BaseModel(models.Model):

    def _cached_FIELD(self, field):
        value = getattr(self, field.attname)
        Model = field.related_model
        return cache.get_model(Model, pk=value)

    class Meta:
        abstract = True


class Course(BaseModel):
    # some fields


class Lesson(BaseModel):
    course = CachedForeignKeyField(Course)

By customizing CachedForeignKeyField, and overwrite the contribute_to_class method, along with BaseModel class with a _cached_FIELD method, every CachedForeignKeyField will automatically have a cached_FIELD property accordingly.

Too good to be true, bravo!