Django ModelForm - Есть ли способ определить поля динамически? (Например, при использовании django-translation-fields)

У меня есть ModelForm с Django 1.11, и я перемещаю несколько полей в другую модель. призвание make_migrations вызывает ошибку, потому что эти поля не существуют в текущей модели. Я добавил некоторые поля в форму, но одно из полей TranslatedField и поэтому в настоящее время есть 2 поля, и в будущем их может быть больше, в зависимости от количества языков. Название поля - город, и в настоящее время я получаю сообщение об ошибке "Unknown field(s) (city_en, city_he) specified for SiteProfile"(потому что я использую 2 языка - "en"и"he") - но я хочу создать все поля динамически с помощью цикла for для языков, которые мы используем в проекте. Могу ли я переопределить (и это хорошо? метод программирования) __new__ метод или есть другой способ? Я предпочитаю не кодировать конкретные имена полей (city_en а также city_he) потому что они могут измениться в будущем, в зависимости от того, сколько языков мы используем.

Вы можете увидеть мой текущий коммит (не работает) на GitHub.

И текущий код этой ветки.

Я хотел бы знать, каков наилучший метод программирования для определения динамического списка полей (все они идентичны, и только одно из них будет использоваться, а другие удаляются в __init__ метод) в ModelForm, где поля сохраняются в другой модели (есть 2 модели, но только одна форма).

Я до сих пор не зафиксировал миграцию из-за этой ошибки при запуске make_migrations.

(Я определил команду make_migrations который только делает makemigrations)

Форма (с моей попытки переопределить __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

Обновление: я думаю об определении этих полей в __init__ метод при удалении их из fields в class Meta, но это хороший подход? Определить поля, которых нет в списке fields?

Джанго предостерегает от явного определения полей.

Настоятельно рекомендуется явно указать все поля, которые должны редактироваться в форме, используя атрибут fields. Невыполнение этого требования может легко привести к проблемам с безопасностью, когда форма неожиданно позволяет пользователю устанавливать определенные поля, особенно когда новые поля добавляются в модель. В зависимости от способа отображения формы проблема может даже не отображаться на веб-странице.

Альтернативный подход заключается в автоматическом включении всех полей или внесении в черный список только некоторых. Известно, что этот фундаментальный подход гораздо менее безопасен и привел к серьезным атакам на крупных веб-сайтах (например, GitHub).

Я хочу знать, есть ли решение без жесткого программирования языков. В настоящее время я жестко закодировал языки:

_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

2 ответа

Обновление 2:... Но поле создается как последнее поле в форме, и я хочу, чтобы оно было посередине. Есть ли способ добавить это поле посередине?

Из https://docs.djangoproject.com/en/2.1/ref/forms/api/:

Если field_order- это список имен полей, поля упорядочены, как указано в списке, а остальные поля добавляются в соответствии с порядком по умолчанию....

Вы можете изменить порядок полей в любое время, используя order_fields() со списком имен полей, как в field_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())

Не знаю, поможет ли это, но у меня есть приложение для опроса, в котором разные пользователи хотят получать разную информацию в форме справочной информации. Я предоставляю это через файл json. Так что в той форме, которую я имею

def __init__(self, *args, **kwargs):
    self.curr_context = kwargs.pop('context', None)
    self.filename = self.get_json_filename()
    super().__init__(*args, **kwargs)
    if os.path.isfile(self.filename) :
        rows = []
        selected_fields = []
        fieldsets = json.load(open(self.filename))
        for fieldset in fieldsets:
            fields = []
            for field in fieldset['fields']:
                if 'field' in field: selected_fields.append(field['field'])
                if 'widget_type' in field:
                    if field['widget_type'] == 'Select': self.fields[field['field']].widget = forms.Select()
                if "choices" in field:
                    choices = []
                    for choice in field['choices']:
                        choices.append((choice['key'],choice['value']))
                    self.fields[field['field']].widget.choices = choices    
                    self.fields[field['field']].choices = choices
                    self.initial[field['field']] = getattr(self.instance, field['field'])
                if 'help' in field:
                    self.fields[field['field']].help_text = field['help']
                if 'html' in field:
                    fields.append(HTML(field['html']))
                if 'divs' in field:
                    fields.append(Field(field['field'], css_class="enabler"))
                    for div in field['divs']:
                        fields.append(Div(Field(div['field'], css_class=div["css"]),*div['div'], css_class="dependent"))
                        selected_fields.append(div['field'])
                        for item in div['div'] : selected_fields.append(item)
                elif 'div' in field:
                    fields.append(Field(field['field'], css_class="enabler"))
                    fields.append(Div(*field['div'], css_class="dependent"))
                    for f in field['div']:
                        selected_fields.append(f)
                else:
                    if 'field' in field: fields.append(field['field'])

            rows.append(Fieldset(fieldset['fieldset'], *fields))

Мой файл JSON будет выглядеть примерно так:

[{
    "fieldset" : "Basic Information",
    "fields" : [
        {
            "field" : "form_filler",
            "div" : ["form_filler_other"]
        },{
            "field" : "child_dob"
        },{
            "field" : "age"
        },{
            "field" : "sex"
        },{
            "field" : "country", 
            "div" : ["zip_code"]
        },{
            "field" : "birth_order"
        }, {
            "field" : "multi_birth_boolean",
            "div" : ["multi_birth"]
        }, {
            "field" : "birth_weight_kg",
            "choices" : [
                {
                    "key" : "",
                    "value" : "--------"
                },{ 
                    "key" : "1.0",
                    "value" : "Henry 1"
                },{ 
                    "key" : "2.0",
                    "value" : "Weight 2"
                },{ 
                    "key" : "3.0",
                    "value" : "Weight 3"
                },{ 
                    "key" : "4.0",
                    "value" : "Weight 4"
                }
            ],
            "widget_type" :  "Select"
        }, {
            "field" : "born_on_due_date", 
            "div" : ["early_or_late", "due_date_diff"]
        }

    ]
}, { ...
}]

field ключ является полем ввода, как и элементы в div список. В этом случае, когда поле выбрано (BooleanField), поля внутри div Список должен быть завершен, что я делаю с помощью чистого метода.

Все мои поля указаны в модели, так что вы можете только выбрать, какие поля использовать

Действительно сложно понять, с чем именно у тебя проблемы. Было бы очень полезно, если бы вы могли ответить на вопросы, которые я задаю ниже.

Я перемещаю несколько полей в другую модель

Какие поля? От какой модели? К какой модели? Я предполагаю, что вы двигаетесь city поле из User модель для SiteProfile,

призвание make_migrations вызывает ошибку, потому что эти поля не существуют в текущей модели

Какая ошибка? Что такое current model ссылаясь на? SiteProfile? Это должно быть вполне выполнимо для перемещения поля от одной модели к другой.

Я посмотрел вокруг вашего хранилища. Особенно ветки, из которых вы пытались мигрировать django-modeltranslation в django-translated-fields, А также нашел вашу проблему на django-translated-fields хранилище на Github.

К сожалению, я не могу полностью понять, в чем проблема у вас. Я думаю, что вашу проблему можно разделить на две независимые.

  1. Миграция не работает для django-translation-fields
  2. Динамическое создание переведенных полей в формах.

Так что, возможно, мы можем начать с миграции. Что вы имеете в виду, когда говорите, что миграция не работает? Можете ли вы показать мне ошибки?

Если вы хотите, чтобы все поля модели в ModelForm динамически менялись при каждом изменении модели, вы можете это проверить. (Пробовал, работает.)

в forms.py

def admin_list_display(model_name):
    list = [field.name for field in model_name._meta.get_fields()]
    return list

class EpisodeForm(forms.ModelForm):
    class Meta:
        model = Episode
        fields = admin_list_display(Episode)

----- Большое предупреждение -------

он покажет ошибку с недоступными для редактирования полями, например (если вы можете изменить функцию, чтобы исключить этот тип полей, отредактируйте ответ.):

created_at = models.DateTimeField(auto_now_add=True)

django.core.exceptions.FieldError: 'created_at' cannot be specified for Episode model form as it is a non-editable field

если вы удалите auto_now_add=True от этого он будет работать.

Запись

он создаст раскрывающийся список для ForiegnKeyField всех параметров.

Другие вопросы по тегам