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>)