Django:ValueError:在尝试创建干净失败时,无法在ForeignField上分配任何内容

在提交表单时,我很难弄清楚如何为ForeignKey字段自动创建模型实例 . 这是一个简单的玩具网站,说明了这个问题:

我有两个型号,Model1和Model2 . Model2包含一个到Model1的ForeignKey . 我希望用户能够通过专门选择要存储在ForeignKey中的Model1实例,或者将该值留空并让自动生成Model1的实例来创建Model2的实例 .

这就是我觉得代码看起来像的样子 . 我的models.py代码非常简单:

# models.py
from django.db import models
from django.core.validators import MinValueValidator


class Model1(models.Model):

    # Note this field cannot be negative
    my_field1 = models.IntegerField(validators=[MinValueValidator(0)])


class Model2(models.Model):
    # blank = True will make key_to_model1 not required on the form,
    # but since null = False, I will still require the ForeignKey
    # to be set in the database.
    related_model1 = models.ForeignKey(Model1, blank=True)

    # Note this field cannot be negative
    my_field2 = models.IntegerField(validators=[MinValueValidator(0)])

forms.py有点牵扯,但是发生的事情非常简单 . 如果Model2Form没有收到Model1的实例,它会尝试在clean方法中自动创建一个,验证它,如果它有效,它会保存它 . 如果它无效,则会引发异常 .

#forms.py
from django import forms
from django.forms.models import model_to_dict

from .models import Model1, Model2


# A ModelForm used for validation purposes only.
class Model1Form(forms.ModelForm):
    class Meta:
        model = Model1


class Model2Form(forms.ModelForm):
    class Meta:
        model = Model2

    def clean(self):
        cleaned_data = super(Model2Form, self).clean()

        if not cleaned_data.get('related_model1', None):

            # Don't instantiate field2 if it doesn't exist.
            val = cleaned_data.get('my_field2', None)
            if not val:
                raise forms.ValidationError("My field must exist")

            # Generate a new instance of Model1 based on Model2's data
            new_model1 = Model1(my_field1=val)

            # validate the Model1 instance with a form form
            validation_form_data = model_to_dict(new_model1)
            validation_form = Model1Form(validation_form_data)

            if not validation_form.is_valid():
                raise forms.ValidationError("Could not create a proper instance of Model1.")

            # set the model1 instance to the related model and save it to the database.
            new_model1.save()
            cleaned_data['related_model1'] = new_model1

        return cleaned_data

但是,这种方法不起作用 . 如果我在表单中输入有效数据,它可以正常工作 . 但是,如果我没有为ForeignKey输入任何内容并为整数设置负值,我会得到一个ValueError .

回溯:文件“/Library/Python/2.7/site-packages/django/core/handlers/base.py”在get_response 111中.response = callback(request,* callback_args,** callback_kwargs)File“/ Library / Python / 2.7 / site-packages / django / views / generic / base.py“视图48.返回self.dispatch(request,* args,** kwargs)文件”/Library/Python/2.7/site-packages/django/views /generic/base.py“在发送69中 . 返回处理程序(请求,* args,** kwargs)文件”/Library/Python/2.7/site-packages/django/views/generic/edit.py“,位于172 . 如果form.is_valid返回super(BaseCreateView,self).post(request,* args,** kwargs)文件“/Library/Python/2.7/site-packages/django/views/generic/edit.py” . ():文件“/Library/Python/2.7/site-packages/django/forms/forms.py”在is_valid 124中 . 返回self.is_bound而不是bool(self.errors)文件“/Library/Python/2.7/site -packages / django / forms / forms.py“在_get_errors 115. self.full_clean()文件”/Library/Python/2.7/site-packages/django/forms/forms.py“in full_clean 272. self._post_cle _post_clean 309中的一个()文件“/Library/Python/2.7/site-packages/django/forms/models.py”.self.instance = construct_instance(self,self.instance,opts.fields,opts.exclude)文件“ /Library/Python/2.7/site-packages/django/forms/models.py“in construct_instance 51. f.save_form_data(instance,cleaning_data [f.name])File”/Library/Python/2.7/site-packages/django /db/models/fields/init.py“在save_form_data中454.setattr(instance,self.name,data)文件”/Library/Python/2.7/site-packages/django/db/models/fields/related.py“在set 362.(instance._meta.object_name,self.field.name))异常类型:ValueError at / add / Exception值:不能赋值None:“Model2.related_model1”不允许空值 .

所以,正在发生的事情是Django正在捕获我的ValidationError,并且即使验证失败,仍然会创建Model2的实例 .

我可以通过覆盖_post_clean方法来修复此问题,以便在出现错误时不创建Model2的实例 . 但是,这个解决方案很难看 . 特别是,_post_clean的行为通常非常有用 - 在更复杂的项目中,我需要_post_clean来运行其他原因 .

我也可以允许ForeignKey为null但在实践中从不将其设置为null . 但是,再次,这似乎是一个坏主意 .

我甚至可以设置一个虚拟的Model1,只要对尝试的新Model1进行验证失败,我就会使用它,但这似乎也是hackish .

一般来说,我可以想到很多黑客来解决这个问题,但我不知道如何以一种相当干净,pythonic的方式解决这个问题 .

回答(1)

2 years ago

基于karthikr在评论中的讨论,我找到了一个我认为可以接受的解决方案 . 我当然仍然愿意接受替代方案 .

我们的想法是在视图中使用逻辑来选择两种形式进行验证:一种形式是标准模型形式,一种是没有ForeignKey字段的模型形式 .

所以,我的models.py是完全相同的 .

我的forms.py有两个Model2表单......一个非常简单,一个没有ForeignKey字段,还有一个新的逻辑,可以为ForeignKey动态生成Model1的新实例 . 新表单的清理逻辑只是我以前在Model2Form中放入的干净逻辑:

#forms.py
from django import forms
from django.forms.models import model_to_dict

from .models import Model1, Model2


# A ModelForm used for validation purposes only.
class Model1Form(forms.ModelForm):
    class Meta:
        model = Model1


class Model2Form(forms.ModelForm):
    class Meta:
        model = Model2

# This inherits from Model2Form so that any additional logic that I put in Model2Form
# will apply to it.
class Model2FormPrime(Model2Form):
    class Meta:
        model = Model2
        exclude = ('related_model1',)

    def clean(self):
        cleaned_data = super(Model2Form, self).clean()

        if cleaned_data.get('related_model1', None):
            raise Exception('Huh? This should not happen...')

        # Don't instantiate field2 if it doesn't exist.
        val = cleaned_data.get('my_field2', None)
        if not val:
            raise forms.ValidationError("My field must exist")

        # Generate a new instance of Model1 based on Model2's data
        new_model1 = Model1(my_field1=val)

        # validate the Model1 instance with a form form
        validation_form_data = model_to_dict(new_model1)
        validation_form = Model1Form(validation_form_data)

        if not validation_form.is_valid():
            raise forms.ValidationError("Could not create a proper instance of Model1.")

        # set the Model1 instance to the related model and save it to the database.
        cleaned_data['related_model1'] = new_model1

        return cleaned_data

    def save(self, commit=True):
        # Best to wait til save is called to save the instance of Model1
        # so that instances aren't created when the Model2Form is invalid
        self.cleaned_data['related_model1'].save()

        # Need to handle saving this way because otherwise related_model1 is excluded
        # from the save due to Meta.excludes
        instance = super(Model2FormPrime, self).save(False)
        instance.related_model1 = self.cleaned_data['related_model1']
        instance.save()

        return instance

然后我的视图逻辑使用两种形式之一来验证,具体取决于发布数据 . 如果它使用Model2FormPrime并且验证失败,它会将数据和错误移动到常规Model2Form以向用户显示:

# Create your views here.
from django.views.generic.edit import CreateView
from django.http import HttpResponseRedirect

from .forms import Model2Form, Model2FormPrime


class Model2CreateView(CreateView):
    form_class = Model2Form
    template_name = 'form_template.html'
    success_url = '/add/'

    def post(self, request, *args, **kwargs):
        if request.POST.get('related_model', None):
            # Complete data can just be sent to the standard CreateView form
            return super(Model2CreateView, self).post(request, *args, **kwargs)
        else:
            # super does this, and I won't be calling super.
            self.object = None

            # use Model2FormPrime to validate the post data without the related model.
            validation_form = Model2FormPrime(request.POST)
            if validation_form.is_valid():
                return self.form_valid(validation_form)
            else:
                # Create a normal instance of Model2Form to be displayed to the user
                # Insantiate it with post data and validation_form's errors
                form = Model2Form(request.POST)
                form._errors = validation_form._errors
                return self.form_invalid(form)

这个解决方案有效,而且非常灵活 . 我可以为我的模型和基础Model2Form添加逻辑,而不必担心破坏它或违反DRY .

但是,它有点难看,因为它要求我使用两种形式来完成一项工作,在表单之间传递错误 . 所以,如果有人可以提出任何建议,我肯定会接受替代解决方案 .