I have a ModelForm
with Django 2.1, and I moved a few fields to another model. Calling make_migrations
causes an error because these fields don't exist in the current model. I added some of the fields to the form, but one of the fields is a TranslatedField
(from django-translated-fields) and therefore there are currently 2 fields, and in the future there might be more, depending on the number of languages. The name of the field is city, and currently I get an error message "Unknown field(s) (city_en, city_he) specified for SiteProfile
" (because I'm using 2 languages - "en" and "he") - but I want to create all the fields dynamically with a for loop over the languages we use in the project. Can I override (and is it a good programming method) the __new__
method or is there another way? I prefer not to hard-code the specific field names (city_en
and city_he
) because they may change in the future, depending on how many languages we use.
You can see my current commit (not working) on GitHub.
And the current code of this branch.
I would like to know what is the best programming method to define a dynamic list of fields (which are all identical, and only one of them will be used, the other are removed in the __init__
method) in a ModelForm where the fields are saved in another model (there are 2 models but only one form).
I still didn't commit the migrations because of this error when running make_migrations.
(I defined a command make_migrations
which only does makemigrations
)
The form (with my trying to override __new__
):
class SpeedyMatchProfileBaseForm(DeleteUnneededFieldsMixin, forms.ModelForm):
user_fields = (
'diet',
'smoking_status',
'marital_status',
*(to_attribute(name='city', language_code=language_code) for language_code, language_name in django_settings.LANGUAGES),
)
validators = {
'height': [speedy_match_accounts_validators.validate_height],
'diet': [speedy_match_accounts_validators.validate_diet],
'smoking_status': [speedy_match_accounts_validators.validate_smoking_status],
'marital_status': [speedy_match_accounts_validators.validate_marital_status],
**{to_attribute(name='profile_description', language_code=language_code): [speedy_match_accounts_validators.validate_profile_description] for language_code, language_name in django_settings.LANGUAGES},
**{to_attribute(name='city', language_code=language_code): [speedy_match_accounts_validators.validate_city] for language_code, language_name in django_settings.LANGUAGES},
**{to_attribute(name='children', language_code=language_code): [speedy_match_accounts_validators.validate_children] for language_code, language_name in django_settings.LANGUAGES},
**{to_attribute(name='more_children', language_code=language_code): [speedy_match_accounts_validators.validate_more_children] for language_code, language_name in django_settings.LANGUAGES},
**{to_attribute(name='match_description', language_code=language_code): [speedy_match_accounts_validators.validate_match_description] for language_code, language_name in django_settings.LANGUAGES},
'gender_to_match': [speedy_match_accounts_validators.validate_gender_to_match],
'min_age_match': [speedy_match_accounts_validators.validate_min_age_match],
'max_age_match': [speedy_match_accounts_validators.validate_max_age_match],
'diet_match': [speedy_match_accounts_validators.validate_diet_match],
'smoking_status_match': [speedy_match_accounts_validators.validate_smoking_status_match],
'marital_status_match': [speedy_match_accounts_validators.validate_marital_status_match],
}
# ~~~~ TODO: diet choices depend on the current user's gender. Also same for smoking status and marital status.
diet = forms.ChoiceField(choices=User.DIET_VALID_CHOICES, widget=forms.RadioSelect(), label=_('My diet'))
smoking_status = forms.ChoiceField(choices=User.SMOKING_STATUS_VALID_CHOICES, widget=forms.RadioSelect(), label=_('My smoking status'))
marital_status = forms.ChoiceField(choices=User.MARITAL_STATUS_VALID_CHOICES, widget=forms.RadioSelect(), label=_('My marital status'))
photo = forms.ImageField(required=False, widget=CustomPhotoWidget, label=_('Add profile picture'))
class Meta:
model = SpeedyMatchSiteProfile
fields = (
'photo',
*(to_attribute(name='profile_description', language_code=language_code) for language_code, language_name in django_settings.LANGUAGES),
*(to_attribute(name='city', language_code=language_code) for language_code, language_name in django_settings.LANGUAGES),
'height',
*(to_attribute(name='children', language_code=language_code) for language_code, language_name in django_settings.LANGUAGES),
*(to_attribute(name='more_children', language_code=language_code) for language_code, language_name in django_settings.LANGUAGES),
'diet',
'smoking_status',
'marital_status',
'gender_to_match',
*(to_attribute(name='match_description', language_code=language_code) for language_code, language_name in django_settings.LANGUAGES),
'min_age_match',
'max_age_match',
'diet_match',
'smoking_status_match',
'marital_status_match',
)
widgets = {
'smoking_status': forms.RadioSelect(),
'marital_status': forms.RadioSelect(),
**{to_attribute(name='profile_description', language_code=language_code): forms.Textarea(attrs={'rows': 3, 'cols': 25}) for language_code, language_name in django_settings.LANGUAGES},
**{to_attribute(name='city', language_code=language_code): forms.TextInput() for language_code, language_name in django_settings.LANGUAGES},
**{to_attribute(name='children', language_code=language_code): forms.TextInput() for language_code, language_name in django_settings.LANGUAGES},
**{to_attribute(name='more_children', language_code=language_code): forms.TextInput() for language_code, language_name in django_settings.LANGUAGES},
**{to_attribute(name='match_description', language_code=language_code): forms.Textarea(attrs={'rows': 3, 'cols': 25}) for language_code, language_name in django_settings.LANGUAGES},
'diet_match': CustomJsonWidget(choices=User.DIET_VALID_CHOICES),
'smoking_status_match': CustomJsonWidget(choices=User.SMOKING_STATUS_VALID_CHOICES),
'marital_status_match': CustomJsonWidget(choices=User.MARITAL_STATUS_VALID_CHOICES),
}
@staticmethod
def __new__(cls, *args, **kwargs):
for language_code, language_name in django_settings.LANGUAGES:
setattr(cls, to_attribute(name='city', language_code=language_code), forms.CharField(label=_('city or locality'), max_length=120))
return super().__new__(*args, **kwargs)
def __init__(self, *args, **kwargs):
self.step = kwargs.pop('step', None)
super().__init__(*args, **kwargs)
self.delete_unneeded_fields()
if ('gender_to_match' in self.fields):
self.fields['gender_to_match'] = forms.MultipleChoiceField(choices=User.GENDER_CHOICES, widget=forms.CheckboxSelectMultiple)
if ('photo' in self.fields):
self.fields['photo'].widget.attrs['user'] = self.instance.user
if ('diet' in self.fields):
update_form_field_choices(field=self.fields['diet'], choices=self.instance.user.get_diet_choices())
self.fields['diet'].initial = self.instance.user.diet
if ('smoking_status' in self.fields):
update_form_field_choices(field=self.fields['smoking_status'], choices=self.instance.user.get_smoking_status_choices())
self.fields['smoking_status'].initial = self.instance.user.smoking_status
if ('marital_status' in self.fields):
update_form_field_choices(field=self.fields['marital_status'], choices=self.instance.user.get_marital_status_choices())
self.fields['marital_status'].initial = self.instance.user.marital_status
if ('diet_match' in self.fields):
update_form_field_choices(field=self.fields['diet_match'], choices=self.instance.get_diet_match_choices())
if ('smoking_status_match' in self.fields):
update_form_field_choices(field=self.fields['smoking_status_match'], choices=self.instance.get_smoking_status_match_choices())
if ('marital_status_match' in self.fields):
update_form_field_choices(field=self.fields['marital_status_match'], choices=self.instance.get_marital_status_match_choices())
for field_name, field in self.fields.items():
if (field_name in self.validators):
field.validators.extend(self.validators[field_name])
field.required = True
Update 1: I'm thinking about defining these fields in the __init__
method while removing them from the fields
in class Meta
, but is it a good approach? To define fields which are not in the list of fields
?
Django warns against defining fields not explicitly.
It is strongly recommended that you explicitly set all fields that should be edited in the form using the fields attribute. Failure to do so can easily lead to security problems when a form unexpectedly allows a user to set certain fields, especially when new fields are added to a model. Depending on how the form is rendered, the problem may not even be visible on the web page.
The alternative approach would be to include all fields automatically, or blacklist only some. This fundamental approach is known to be much less secure and has led to serious exploits on major websites (e.g. GitHub).
I want to know if there is a solution without hard-coding the languages. Currently I hard-coded the languages:
_city = forms.CharField(label=_('City or locality'), max_length=120, error_messages={'required': _("Please write where you live.")})
city_en = _city
city_he = _city
https://github.com/speedy-net/speedy-net/blob/staging/speedy/match/accounts/forms.py#L64-L66
Update 2: I found out that I can add this field dynamically by adding this line in the __init__
method of the form:
# Create the localized city field dynamically.
self.fields[to_attribute(name='city')] = forms.CharField(label=_('City or locality'), max_length=120, error_messages={'required': _("Please write where you live.")})
And then removing it from the fields list in class Meta
and from the hard-coded definition in the form itself. But, the field is created as the last field in the form and I want it to be in the middle. Is there a way to add this field in the middle?
Update 2: ... But, the field is created as the last field in the form and I want it to be in the middle. Is there a way to add this field in the middle?
From https://docs.djangoproject.com/en/2.1/ref/forms/api/#notes-on-field-ordering:
If
field_order
is a list of field names, the fields are ordered as specified by the list and remaining fields are appended according to the default order. ...You may rearrange the fields any time using
order_fields()
with a list of field names as infield_order
.
class SpeedyMatchProfileBaseForm(DeleteUnneededFieldsMixin, forms.ModelForm):
...
def __init__(self, *args, **kwargs):
...
# Create the localized city field dynamically.
self.fields[to_attribute('city')] = forms.CharField(label=_('City or locality'), max_length=120, error_messages={'required': _("Please write where you live.")})
# Rearrange the fields.
# self.order_fields((
# 'photo',
# to_attribute('profile_description'),
# to_attribute('city')),
# # Remaining fields are appended according to the default order.
# )
self.order_fields(field_order=self.get_fields())