Reputation: 1319
I'd like to add to existing models new CharFields via one common mixin or abstract model but names of these fields depend on configuraton. so one model will have someprefix1_title field and another model - someprefix2_title.
Is it possible to make this approach to work:
class AbstractModel(models.Model):
self.fields_prefix + '_title' = models.CharField(max_length=255, blank=True, default='')
class Meta:
abstract = True
class ModelOne(AbstractModel):
fields_prefix = 'someprefix1'
id = models.AutoField(primary_key=True)
class ModelTwo(AbstractModel):
fields_prefix = 'someprefix2'
id = models.AutoField(primary_key=True)
so ModelOne could have fields id and someprefix1_title.
upd: what about monkey-patching with add_to_class() will it work or it's an antipattern and should not be used?
Upvotes: 11
Views: 5042
Reputation: 6392
Another couple approaches I've had success with, based on the fantastic top answer from @whp, adding fields using a field, method or decorator. My use case was storing multiple fields for translations in the most plain, vanilla way possible, along with a locale-sensitive property.
My preferred approach is a field, because this is where most developers would expect this to be:
class TranslatedField:
"""
A Django pseudo field that adds a field per supported language to a model, and a getter property that
returns the translation for the language currently selected in Django (which is set by Django
LocaleMiddleware).
Usage example:
# models.py
# models.py
class Acme(models.Model):
code = CodeField()
name = TranslatedField(models.CharField(blank=True, verbose_name=_("common name")))
description = TranslatedField(models.CharField(max_length=800, verbose_name=_("description")))
# Usage:
obj = Acme(code="abc", name_en="English name", name_pt="Nome português")
obj.name
>> "English name"
translation.activate("pt")
obj.name
>> "Nome português"
"""
def __init__(self, field):
self.field = field
def contribute_to_class(self, cls, name, private_only=False):
# Add language fields to cls and db, of type self.field, eg, obj.name_en = CharField()
for language_code, language_name in settings.LANGUAGES:
model_field_name = f"{name}_{language_code}"
field = self.field.clone()
field.verbose_name = f"{self.field.verbose_name} ({language_name})"
field.contribute_to_class(cls=cls, name=model_field_name, private_only=private_only)
# Add property that returns local translation, eg, obj.name == "Nome português"
def local_translation_getter(obj):
# Note that translation.get_language() can return, eg, en-us if this is in settings.LANGUAGE_CODE or settings.LANGUAGES[i][0] - you'll need to either strip after "-" or remove the locale code
current_language_code = translation.get_language()
model_field = f"{name}_{current_language_code}"
return getattr(obj, model_field, "") # translation.get_language() returns default language if none set
setattr(cls, name, property(local_translation_getter))
return cls
Alternatively a decorator, as decoration is a standard way of adding functionality to a class, and they have to appear right next to the model definition:
from django.conf import settings
from from django.db import models # or django.contrib.gis.db.models
from django.utils import translation
def translatable_field(field_name, field):
"""
Decorator to add translated fields and property getter for current language to a Django model.
Usage example:
# models.py
@translatable_field("name", models.CharField(blank=True, verbose_name=_("common name")))
@translatable_field("description", models.TextField())
class Acme(models.Model):
code = models.CharField()
# Usage:
obj = Acme(code="abc", name_en="English name", name_pt="nome português")
obj.name
>> "English name"
translation.activate("pt")
obj.name
>> "nome português"
"""
def decorate(model):
# Add translated fields to model, eg, name_en
for language_code, name in settings.LANGUAGES:
model_field_name = f"{field_name}_{language_code}"
field.clone().contribute_to_class(model, model_field_name)
# Add property that returns local translation, eg, name
def local_translation_getter(self):
# Note that translation.get_language() can return, eg, en-us if this is in settings.LANGUAGE_CODE or settings.LANGUAGES[i][0] - you'll need to either strip after "-" or remove the locale code
model_field = f"{field_name}_{translation.get_language()}"
return getattr(self, model_field, "")
setattr(model, field_name, property(local_translation_getter))
return model
return decorate
A method that does exactly the same:
def add_translatable_field_to_model(model, field_name, field):
"""
This is functionally identical, except usage syntax.
Example usage:
# models.py
class Acme(models.Model):
code = models.CharField()
add_translatable_field_to_model(Acme, "name", models.CharField(blank=True, verbose_name=_("common name")))
add_translatable_field_to_model(Acme, "description", models.TextField())
"""
for code, name in settings.LANGUAGES:
field.clone().contribute_to_class(model, f"{field_name}_{code}")
def local_translation_getter(self):
# Note that translation.get_language() can return, eg, en-us if this is in settings.LANGUAGE_CODE or settings.LANGUAGES[i][0] - you'll need to either strip after "-" or remove the locale code
language_code = translation.get_language()
return getattr(self, f"{field_name}_{language_code}", "")
setattr(model, field_name, property(local_translation_getter))
Here is a great write-up from Django on this machinery:
https://code.djangoproject.com/wiki/DevModelCreation
Upvotes: 0
Reputation: 1514
Try using a factory pattern to set up your different versions of AbstractModel
.
With this approach, you can more strictly control the way AbstractModel
is modified by way of the factory function dynamic_fieldname_model_factory
.
We're also not modifying ModelOne
or ModelTwo
after their definitions -- other solutions have pointed out that this helps avoid maintainability problems.
models.py:
from django.db import models
def dynamic_fieldname_model_factory(fields_prefix):
class AbstractModel(models.Model):
class Meta:
abstract = True
AbstractModel.add_to_class(
fields_prefix + '_title',
models.CharField(max_length=255, blank=True, default=''),
)
return AbstractModel
class ModelOne(dynamic_fieldname_model_factory('someprefix1')):
id = models.AutoField(primary_key=True)
class ModelTwo(dynamic_fieldname_model_factory('someprefix2')):
id = models.AutoField(primary_key=True)
Here is the migration generated by this code:
# Generated by Django 2.1.7 on 2019-03-07 19:53
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='ModelOne',
fields=[
('someprefix1_title', models.CharField(blank=True, default='', max_length=255)),
('id', models.AutoField(primary_key=True, serialize=False)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ModelTwo',
fields=[
('someprefix2_title', models.CharField(blank=True, default='', max_length=255)),
('id', models.AutoField(primary_key=True, serialize=False)),
],
options={
'abstract': False,
},
),
]
Upvotes: 11
Reputation: 1996
Django models can be created with dynamic field names . Here is a simple Django model:
class Animal(models.Model):
name = models.CharField(max_length=32)
And here is the equivalent class built using type()
:
attrs = {
'name': models.CharField(max_length=32),
'__module__': 'myapp.models'
}
Animal = type("Animal", (models.Model,), attrs)
Any Django model that can be defined in the normal fashion can be made using type()
.
To run migrations:South has a reliable set of functions to handle schema and database migrations for Django projects. When used in development, South can suggest migrations but does not attempt to automatically apply them
from south.db import db
model_class = generate_my_model_class()
fields = [(f.name, f) for f in model_class._meta.local_fields]
table_name = model_class._meta.db_table
db.create_table(table_name, fields)
# some fields (eg GeoDjango) require additional SQL to be executed
db.execute_deferred_sql()
Upvotes: 7
Reputation: 3447
Technically, Its bad practice by creating model fields dynamically because this breaks the standard rule of keeping the database schema history using the django's migration process.
All you need is to store some fields under a django model which have flexibility to store dynamic fields. So I would suggest you to use the HStoreField.
Using HStoreField
you can store data in a key-value pair format or json. So this will resolve the problem of storing the dynamic fields.
Here is an example given by Django docs.
from django.contrib.postgres.fields import HStoreField
from django.db import models
class Dog(models.Model):
name = models.CharField(max_length=200)
data = HStoreField()
def __str__(self):
return self.name
This is how you can query the HStoreFields.
>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})
>>> Dog.objects.create(name='Meg', data={'breed': 'collie'})
>>> Dog.objects.filter(data__breed='collie')
<QuerySet [<Dog: Meg>]>
I hope this would resolve your problem. Please do let me know if you have any queries.
Thanks
Upvotes: 0
Reputation: 50806
The cleanest way would probably be using add_to_class()
:
ModelOne.add_to_class(
'%s_title' % field_prefix,
models.CharField(max_length=255, blank=True, default='')
)
Still this can be considered "monkey-patching" with all its downsides like making the app more difficult to maintain, have code that is more difficult to understand etc... Bu if your use case makes it really necessary to do something like that it would probably be the best solution as add_to_class()
is some functionality provided from Django itself and has been stable for quite some time.
Upvotes: 4