How to properly dynamically add ModelForm fields?
kunambi opened this issue · 7 comments
I'm attempting to dynamically add translatable fields for my forms.ModelForm
, depending on whether the customer has enabled the language or not. However, the translated value isn't saved to the model.
from copy import deepcopy
from django import forms
from modeltranslation.fields import TranslationField
class DefaultModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if user := self.request and self.request.user:
company = user.company
app_setting = company.settings
default_lang = settings.MODELTRANSLATION_DEFAULT_LANGUAGE # "en"
default_search = f"_{default_lang}"
to_add = []
"""Set field_order to the same order as set in Meta.fields, unless already set"""
if self.field_order is None:
self.field_order = list(self.Meta.fields)
for modelfield in self._meta.model._meta.fields:
"""If we found a default translation field, add it to the list"""
if (
(formfield := self.fields.get(modelfield.name, None))
and modelfield.name.endswith(default_search)
and isinstance(modelfield, TranslationField)
):
to_add.append(
{
"position": self.field_order.index(modelfield.name),
"formfield": deepcopy(formfield),
"name": modelfield.name.removesuffix(default_search), # "description"
"languages": app_setting.get_additional_language_codes, # ["es"]
}
)
for addable in to_add:
for lang in addable.get("languages"):
field_name = f"{addable.get('name')}_{lang}" # "description_es"
formfield = addable.get("formfield")
formfield.label = formfield.label.replace(f"[{default_lang}]", f"[{lang}]")
formfield.required = False
formfield.initial = getattr(self.instance, field_name, "")
self.fields[field_name] = formfield
self.field_order.insert(addable.get("position", 0) + 1, field_name)
self.order_fields(self.field_order)
This code allows me to render the fields accordingly. If the customer has selected to show e.g. "es"
(Spanish), the translatable field ("description_en"
) will be copied and I create a new field ("description_es"
) in the right position inside the field_order
. So far, all is well.
But when I POST the form, this is what happens inside my view:
def form_valid(self, form):
is_updating = True if form.instance.pk else False
self.object = form.save()
if is_updating:
# Todo: print message
pass
breakpoint()
"""
(Pdb++) form.is_valid()
True
(Pdb++) form.cleaned_data
{'company': <Company: Test>, 'description_en': 'I have a library!', 'description_es': 'Tengo una biblioteca!'}
(Pdb++) self.object = form.save()
(Pdb++) self.object
<Venue: TestVenue>
(Pdb++) self.object.description_en
'I have a library!'
(Pdb++) self.object.description_es
''
"""
return super().form_valid(form)
Here is what I don't understand: why isn't the description_es
value saved to the object?
I tried creating the field through
modelfield = getattr(self._meta.model, f"{addable.get('name')}_{default_lang}")
translated_field = create_translation_field(
model=self._meta.model,
field_name=addable.get("name"),
lang=lang,
empty_value=None if modelfield.field and modelfield.field.null else "",
)
formfield = translated_field.formfield()
But with the same erroneous result
Try looking into form.save to understand why field isnt' saved.
form.save()
is called on the django.forms.models.BaseModelForm
, which only calls self.instance.save()
(if no errors are found).
No errors are encountered, but the data of the instance isn't updated.
What am I missing?
Look at what's happening before that. I think it's BaseModelForm._post_clean
method.
Check if all fields are available in cleaned_data.
But before we get too far, try using modelform_factory instead of adding fields in __init__
. Maybe it will just work.
Look at what's happening before that. I think it's BaseModelForm._post_clean method. Check if all fields are available in cleaned_data.
As I showed in the first post, the cleaned_data
does indeed contain the value for description_es
. The problem is that it doesn't seem to be saved.
But before we get too far, try using modelform_factory instead of adding fields in
__init__
. Maybe it will just work.
modelform_factory
is not what I want to use in this scenario. I have other uses of it in other locations. Why do you think it will work?
It seems like there's some kind of magic happening when form.save()
is run. django-modeltranslation
is supposedly patching how the models are saved, hopefully I'll understand the source code
I managed to find a solution. The problem was with django.forms.models.BaseModelForm
which removes any fields which aren't defined in Meta.fields
through the use of object_data = model_to_dict(instance, opts.fields, opts.exclude)
.
Solution became:
# ...
for addable in to_add:
for lang in addable.get("languages"):
field_name = f"{addable.get('name')}_{lang}" # "description_es"
formfield = addable.get("formfield")
formfield.label = formfield.label.replace(f"[{default_lang}]", f"[{lang}]")
formfield.required = False
formfield.initial = getattr(self.instance, field_name, "")
self.fields[field_name] = formfield
self.field_order.insert(addable.get("position", 0) + 1, field_name)
# these lines made the difference
if field_name not in self._meta.fields:
self._meta.fields = self._meta.fields + (field_name,)
self.order_fields(self.field_order)