Django ModelForm - 有没有办法动态定义字段,但不是最后一个字段?

Django ModelForm - Is there a way to define fields dynamically, but not as the last field?

我有一个使用 Django 2.1 的 ModelForm,我将一些字段移到了另一个模型。调用 make_migrations 会导致错误,因为这些字段在当前模型中不存在。我在表单中添加了一些字段,但其中一个字段是 TranslatedField(来自 django-translated-fields),因此目前有 2 个字段,将来可能会有更多,具体取决于语言的数量。该字段的名称是城市,目前我收到一条错误消息“Unknown field(s) (city_en, city_he) specified for SiteProfile”(因为我使用了 2 种语言 - "en" 和 "he") - 但我想创建所有这些字段动态地使用 for 循环遍历我们在项目中使用的语言。我可以覆盖(这是一种很好的编程方法)__new__ 方法还是有其他方法?我不希望对特定字段名称(city_encity_he)进行硬编码,因为它们将来可能会发生变化,具体取决于我们使用的语言数量。

您可以在 GitHub 上看到我当前的 commit(不工作)。

以及本分支的当前code

我想知道定义动态字段列表的最佳编程方法是什么(它们都是相同的,只会使用其中一个,其他在__init__方法中删除) 在 ModelForm 中,字段保存在另一个模型中(有 2 个模型,但只有一种形式)。

由于 运行 make_migrations.

时出现此错误,我仍然没有提交迁移

(我定义了一个只执行 makemigrations 的命令 make_migrations

表单(我试图覆盖 __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

更新 1: 我正在考虑在 __init__ 方法中定义这些字段,同时将它们从 class Meta 中的 fields 中删除,但这是一个好方法吗?定义不在 fields?

列表中的字段

Django warns 反对未明确定义字段。

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).

我想知道是否有不对语言进行硬编码的解决方案。目前我对语言进行了硬编码:

_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

更新 2: 我发现我可以通过在表单的 __init__ 方法中添加此行来动态添加此字段:

# 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.")})

然后将其从 class Meta 中的字段列表和表单本身的硬编​​码定义中删除。但是,该字段是作为表单中的最后一个字段创建的,我希望它位于中间。有没有办法在中间添加这个字段?

我不知道这是否有帮助,但我有一个调查应用程序,不同的用户需要不同的背景信息表信息。我通过 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 列表中的项目也是如此。 In this case when the field is selected (BooleanField) the fields within the div list must be completed which I do through the clean method.

我的所有字段都在模型中指定,因此您只能挑选要使用的字段

真的很难理解你到底有什么问题。如果您能回答我在下面提出的问题,那将非常有帮助。

I'm moving a few fields to another model

什么领域?来自哪个型号?给哪个型号? 我假设您正在将 city 字段从 User 模型移动到 SiteProfile.

Calling make_migrations causes an error because these fields don't exist in the current model

什么错误? current model 指的是什么? SiteProfile?将字段从一个模型移动到另一个模型应该是完全可行的。

我查看了您的存储库。特别是您尝试从 django-modeltranslation 迁移到 django-translated-fields 的分支。并且还在 Github.

django-translated-fields 存储库中发现了你的问题

很遗憾,我无法完全理解您遇到的问题。 我认为你的问题可以细分为两个独立的问题。

  1. 迁移不适用于 django-translated-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 创建下拉列表。

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?

来自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 in 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())