[Django]-Dynamically extending django models using a metaclass

3đź‘Ť

âś…

Tricky!

The problem there is that when you try to .clone the field in the models, they have not being fully initialized, and thus, Django mechanisms of finding a foreign-model by its string qualifeid name can’t be used. The problem is that the code does not check if the foreignmodel is passed as an class reference, instead of a string.

The only way to workaround this seems to be monkey-patching these checks when the classes are being created.
While at that, when clonning a foreign-object field, the related_field – a backreference created automatically by django ORM so one can get from the “pointed-at” object to the “holder” object have to be passed explicitly to the new, cloned, field. Else it would point to the original field instead.
This requires a bit more of monkeypatching, to insert an explicit “related_name” parameter in the inner workings of the .clone call.

Those 2 things being acomplished, it seems to work. Here is is the code I used, based on yours:

from django.db import models
from django.db.models.fields import related
from unittest.mock import patch

class ResMetaclass(models.base.ModelBase):
    def __new__(cls, name, bases, attrs):

        fields = {
            k: v for k, v in attrs.items() if not k.startswith('_') and isinstance(v, models.Field)
        }
        new_fields = {}
        for field_name, field in fields.items():
            new_field_name = field_name + "_new"
            if not isinstance(field, related.RelatedField):
                new_fields[new_field_name] = field.clone()
            else:
                real_deconstruct = field.deconstruct
                def _deconstruct():
                    name, path, args, kwargs = real_deconstruct()
                    kwargs["related_name"] = new_field_name
                    return name, path, args, kwargs

                with patch("django.apps.registry.apps.check_models_ready", lambda: True):
                    field.deconstruct = _deconstruct
                    # Assume foregnKeys are always within the same file, and
                    # disable model-ready checking:
                    new_fields[new_field_name] = field.clone()
                    del field.deconstruct


        attrs_extended = {
            **attrs,
            **new_fields
        }

        bases = (models.Model,)
        clsobj = super().__new__(cls, name, bases, attrs_extended)

        return clsobj

class EntityBase(models.Model, metaclass=ResMetaclass):
    class Meta:
        abstract = True

class Pizza(EntityBase):
    name = models.CharField(max_length=10)
    price = models.DecimalField(max_digits=10, decimal_places=2)


class MenuEntry(EntityBase):
    entry_number =  models.IntegerField()
    pizza = models.ForeignKey("Pizza", on_delete="cascade")

And the resulting fields on the MenuEntry class:

In [1]: from test1.models import Pizza, MenuEntry                                                                                                      

In [2]: MenuEntry._meta.fields                                                                                                                         
Out[2]: 
(<django.db.models.fields.AutoField: id>,
 <django.db.models.fields.IntegerField: entry_number>,
 <django.db.models.fields.related.ForeignKey: pizza>,
 <django.db.models.fields.IntegerField: entry_number_new>,
 <django.db.models.fields.related.ForeignKey: pizza_new>)
👤jsbueno

Leave a comment