Reputation: 129795
I'm trying to dynamically generate a new Model, based on fields from an existing Model. Both are defined in /apps/main/models.py
. The existing model looks something like this:
from django.db import models
class People(models.Model):
name = models.CharField(max_length=32)
age = models.IntegerField()
height = models.IntegerField()
I have a list containing the names of fields that I would like to copy:
target_fields = ["name", "age"]
I want to generate a new model the has all of the Fields named in target_fields
, but in this case they should be indexed (db_index = True
).
I originally hoped that I would just be able to iterate over the class properties of People
and use copy.copy
to copy the field descriptions that are defined on it. Like this:
from copy import copy
d = {}
for field_name in target_fields:
old_field = getattr(People, field_name) # alas, AttributeError
new_field = copy(old_field)
new_field.db_index = True
d[field_name] = new_field
IndexedPeople = type("IndexedPeople", (models.Model,), d)
I wasn't sure if copy.copy()
ing Fields would work, but I didn't get far enough to find out: the fields listed in the class definition don't aren't actually included as properties on the class object. I assume they're used for some metaclass shenanigans instead.
After poking around in the debugger, I found some type of Field objects listed in People._meta.local_fields
. However, these aren't just simple description that can be copy.copy()
ed and used to describe another model. For example, they include a .model
property referring to People
.
How can I create a field description for a new model based on a field of an existing model?
Upvotes: 5
Views: 1933
Reputation: 1526
There is build in way for fields copying Field.clone()
- method which deconstructs field removing any model dependent references:
def clone(self):
"""
Uses deconstruct() to clone a new copy of this Field.
Will not preserve any class attachments/attribute names.
"""
name, path, args, kwargs = self.deconstruct()
return self.__class__(*args, **kwargs)
So you can use following util to copy fields ensuring that you'll not accidentally affect source fields of model you're copying from:
def get_field(model, name, **kwargs):
field = model._meta.get_field(name)
field_copy = field.clone()
field_copy.__dict__.update(kwargs)
return field_copy
Also can pass some regular kwargs like verbose_name and etc:
def get_field_as_nullable(*args, **kwargs):
return get_field(*args, null=True, blank=True, **kwargs)
Does not work for m2m fields inside of model definition. (m2m.clone() on model definition raises AppRegistryNotReady: Models aren't loaded yet
)
Well, depends on case. Some times you don't need inheristance but actuall fields copying. When? For example:
I have a User model and model which represents an application (document for user data update request) for user data update:
class User(models.Model):
first_name = ...
last_name = ...
email = ...
phone_number = ...
birth_address = ...
sex = ...
age = ...
representative = ...
identity_document = ...
class UserDataUpdateApplication(models.Model):
# This application must ONLY update these fields.
# These fiends must be absolute copies from User model fields.
user_first_name = ...
user_last_name = ...
user_email = ...
user_phone_number = ...
So, i shouldn't carry out duplicated fields from my User model to abstract class due to the fact that some other non-user-logic-extending model wants to have exact same fields. Why? Because it's not directly related to User model - User model shouldn't care what depends on it (excluding cases when you want to extend User model), so it shouldn't be separated due to fact that some other model with it's own non User related logic want's to have exact same fields.
Instead you can do this:
class UserDataUpdateApplication(models.Model):
# This application must ONLY update these fields.
user_first_name = get_field(User, 'first_name')
user_last_name = get_field(User, 'last_name')
user_email = get_field(User, 'user_email')
user_phone_number = get_field(User, 'phone_number')
You also would make som util which would generate some abc class "on fly" to avoid code duplication:
class UserDataUpdateApplication(
generate_abc_for_model(
User,
fields=['first_name', 'last_name', 'email', 'phone_number'],
prefix_fields_with='user_'),
models.Model,
):
pass
Upvotes: 4
Reputation: 129795
From poking around in the debugger and the source: all Django models use the ModelBase
metaclass defined in /db/models/base.py
. For each field in a model's class definition, ModelBase
's .add_to_class
method will call the field's .contribute_to_class
method.
Field.contribute_to_class
is defined in /db/models/fields/__init__.py
and it is what's responsible for associating a field definition with a particular model. The field is modified by adding the .model
property and by calling the .set_attributes_from_name
method with the name used in the model's class definition. This in turn adds adds the .attname
and .column
properties and sets .name
and .verbose_name
if necessary.
When I inspect the __dict__
property of a newly-defined CharField
and compare it with that of a CharField
that was already associated with a model, I also see that these are the only differences:
.creation_counter
property is unique for each instance..attrname
, .column
and .model
properties do not exist on the new instance..name
and .verbose_name
properties is None
on the new instance.It doesn't seem possible to distinguish between .name
/.verbose_name
properties that were manually specified to the constructor and ones that were automatically generated. You'll need to chose either to always reset them, ignoring any manually-specified values, or never clear them, which would cause them to always ignore any new name they were given in the new model. I want to use the same name as the original fields, so I am not going to touch them.
Knowing what differences exist, I am using copy.copy()
to clone the existing instance, then apply these changes to make it behave like a new instance.
import copy
from django.db import models
def copy_field(f):
fp = copy.copy(f)
fp.creation_counter = models.Field.creation_counter
models.Field.creation_counter += 1
if hasattr(f, "model"):
del fp.attname
del fp.column
del fp.model
# you may set .name and .verbose_name to None here
return fp
Given this function, I create the new Model with the following:
target_field_name = "name"
target_field = People._meta.get_field_by_name(target_field_name)[0]
model_fields = {}
model_fields["value"] = copy_field(target_field)
model_fields["value"].db_index = True
model_fields["__module__"] = People.__module__
NewModel = type("People_index_" + field_name, (models.Model,), model_fields)
It works!
Upvotes: 6