python - Django:ValueError:尝试在干净的环境中创建时无法在 ForeignField 上分配任何内容失败

标签 python django django-forms

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

我有两个模型,模型 1 和模型 2。 Model2 包含 Model1 的 ForeignKey。我希望用户能够通过专门选择 Model1 的实例存储在 ForeignKey 中,或者将该值留空并让 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。

Traceback: File "/Library/Python/2.7/site-packages/django/core/handlers/base.py" in get_response 111. response = callback(request, *callback_args, **callback_kwargs) File "/Library/Python/2.7/site-packages/django/views/generic/base.py" in view 48. return self.dispatch(request, *args, **kwargs) File "/Library/Python/2.7/site-packages/django/views/generic/base.py" in dispatch 69. return handler(request, *args, **kwargs) File "/Library/Python/2.7/site-packages/django/views/generic/edit.py" in post 172. return super(BaseCreateView, self).post(request, *args, **kwargs) File "/Library/Python/2.7/site-packages/django/views/generic/edit.py" in post 137. if form.is_valid(): File "/Library/Python/2.7/site-packages/django/forms/forms.py" in is_valid 124. return self.is_bound and not bool(self.errors) File "/Library/Python/2.7/site-packages/django/forms/forms.py" in _get_errors 115. self.full_clean() File "/Library/Python/2.7/site-packages/django/forms/forms.py" in full_clean 272. self._post_clean() File "/Library/Python/2.7/site-packages/django/forms/models.py" in _post_clean 309. self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude) File "/Library/Python/2.7/site-packages/django/forms/models.py" in construct_instance 51. f.save_form_data(instance, cleaned_data[f.name]) File "/Library/Python/2.7/site-packages/django/db/models/fields/init.py" in save_form_data 454. setattr(instance, self.name, data) File "/Library/Python/2.7/site-packages/django/db/models/fields/related.py" in set 362. (instance._meta.object_name, self.field.name))

Exception Type: ValueError at /add/ Exception Value: Cannot assign None: "Model2.related_model1" does not allow null values.

所以,发生的事情是 Django 捕获了我的 ValidationError 并仍在创建 Model2 的实例,即使验证失败也是如此。

我可以通过重写 _post_clean 方法来解决这个问题,以便在出现错误时不创建 Model2 的实例。但是,这个解决方案很丑陋。特别是,_post_clean 的行为通常非常有用——在更复杂的项目中,出于其他原因我需要运行 _post_clean。

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

我什至可以设置一个虚拟 Model1,每当对尝试的新 Model1 的验证失败时我都会使用它,但这看起来也很老套。

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

最佳答案

基于 karthikr 在评论中的讨论,我找到了一个我认为可以接受的解决方案。我绝对对其他选择持开放态度。

想法是在 View 中使用逻辑在两种形式之间进行选择以进行验证:一种形式是标准模型形式,一种是没有 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

然后我的 View 逻辑使用两种形式之一进行验证,具体取决于发布数据。如果它使用 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。

不过,它有点难看,因为它要求我使用两种形式来完成一种工作,即在形式之间传递错误。因此,如果有人可以提出任何建议,我绝对愿意接受其他解决方案。

关于python - Django:ValueError:尝试在干净的环境中创建时无法在 ForeignField 上分配任何内容失败,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15817437/

相关文章:

python - 计算二元组频率

python - Ray Cluster如何访问所有节点资源

python - 是否可以在 Google AppEngine 上使用 django Piston?

python - Django:在没有 javascript 的情况下添加内联表单集行

python - django 覆盖模型表单或字段中的默认错误消息

java - TensorFlow Lite 在机器学习模型处理数值数据方面有用吗?

python - 为什么不能迭代?

python - Amazon Elastic Beanstalk 中的 Django Collectstatic 不工作

python - Django/db-api 无法创建数据库?

Django基于模型实例限制ModelForm中ManyToMany字段的选项