简体   繁体   English

Django-验证模型和ModelForm中计算字段的唯一性

[英]Django - validate unique for a calculated field in the Model and also in the ModelForm

TL;DR both my model and my form calculate the value of the field number_as_char . TL; DR我的模型和表单都计算字段number_as_char的值。 Can I avoid the double work, but still check uniqueness when using the model without the form? 我可以避免双重工作,但在使用不带表格的模型时仍能检查其唯一性吗?

I use Python 3 and Django 1.11 我使用Python 3和Django 1.11


My model looks as follows: 我的模型如下所示:

class Account(models.Model):
    parent_account = models.ForeignKey(
        to='self',
        on_delete=models.PROTECT,
        null=True,
        blank=True)
    number_suffix = models.PositiveIntegerField()
    number_as_char = models.CharField(
        max_length=100,
        blank=True,
        default='',
        unique=True)

    @classmethod
    def get_number_as_char(cls, parent_account, number_suffix):
        # iterate over all parents
        suffix_list = [str(number_suffix), ]
        parent = parent_account
        while parent is not None:
            suffix_list.insert(0, str(parent.number_suffix))
            parent = parent.parent_account

        return '-'.join(suffix_list)

    def save(self, *args, **kwargs):
        self.number_as_char = self.get_number_as_char(
            self.parent_account, self.number_suffix)
        super().save(*args, **kwargs)

The field number_as_char is not supposed to be set by the user because it is calculated based on the selected parent_account : it is obtained by chaining the values of the field number_suffix of all the parent accounts and the current instance. 字段number_as_char不应由用户设置,因为它是根据所选的parent_account计算的:它是通过链接所有父帐户和当前实例的字段number_suffix的值而获得的。

Here is an example with three accounts: 这是一个具有三个帐户的示例:

ac1 = Account()
ac1.parent_account = None
ac1.number_suffix = 2
ac1.save()
# ac1.number_as_char is '2'

ac2 = Account()
ac2.parent_account = ac1
ac2.number_suffix = 5
ac2.save()
# ac2.number_as_char is '2-5'

ac3 = Account()
ac3.parent_account = ac2
ac3.number_suffix = 1
ac3.save()
# ac3.number_as_char is '2-5-1'

It is NOT an option to drop the field and use a model property instead, because I need to ensure uniqueness and also use that field for sorting querysets with order_by() . 不是删除该字段并使用model属性的选项,因为我需要确保唯一性,并且还需要使用该字段对order_by()进行排序。


My form looks as follows: 我的表格如下:

class AccountForm(forms.ModelForm):

    class Meta:
        model = Account
        fields = [
            'parent_account', 'number_suffix', 'number_as_char',
        ]
        widgets = {
            'number_as_char': forms.TextInput(attrs={'readonly': True}),
        }

    def clean(self):
        super().clean()
        self.cleaned_data['number_as_char'] = self.instance.get_number_as_char(
            self.cleaned_data['parent_account'], self.cleaned_data['number_suffix'])

I included number_as_char in the form with widget attribute readonly and I use the forms clean() method to calculate number_as_char (it has to be calculated before validating uniqueness). 我在窗体中包含number_as_char且其部件属性为readonly并且我使用表单clean()方法来计算number_as_char (必须在验证唯一性之前进行计算)。


This all works (the model and the form), but after validating the form, the value of number_as_char will be calculated again by the models save() method. 所有这些(模型和表单)都有效,但是在验证表单之后,将通过模型的save()方法再次计算number_as_char的值。 Its not a big problem, but is there a way to avoid this double calculation? 它不是一个大问题,但是有办法避免这种双重计算吗?

  1. If I remove the calculation from the forms clean() method, then the uniqueness will not be validated with the new value (it will only check the old value). 如果我从表单clean()方法中删除该计算,则不会使用新值来验证唯一性(它将仅检查旧值)。
  2. I don't want to remove the calculation entirely from the model because I use the model in other parts without the form. 我不想从模型中完全删除计算,因为我在没有表单的其他部分中使用了该模型。

Do you have any suggestions what could be done differently to avoid double calculation of the field? 您有什么建议可以采取其他措施来避免对该字段进行重复计算吗?

I can't see any way around doing this in two places ( save() and clean() ) given that you need it to work for non-form-based saves as well). 我看不到在两个地方都可以做到这一点( save()clean() ),因为您也需要它来进行基于非表单的保存。

However I can offer two efficiency improvements to your get_number_as_char method: 但是,我可以为您的get_number_as_char方法提供两个效率改进:

  1. Make it a cached_property so that the second time it is called, you simply return a cached value and eliminate double-calculation. 将其设置为cached_property以便在第二次调用它时,只需返回一个缓存的值并消除重复计算。 Obviously you need to be careful that this isn't called before an instance is updated, otherwise the old number_as_char will be cached. 显然,您需要注意实例更新之前不会调用此方法,否则将缓存旧的number_as_char This should be fine as long as get_number_as_char() is only called during a save/clean. 只要只在保存/清除期间调用get_number_as_char()就可以了。

  2. Based on the information you've provided above you shouldn't have to iterate over all the ancestors, but can simply take the number_as_char for the parent and append to it. 根据上面提供的信息,您不必遍历所有祖先,而只需将number_as_char作为父项number_as_char即可。

The following incorporates both: 以下内容兼有:

@cached_property
def get_number_as_char(self, parent_account, number_suffix):
    number_as_char = str(number_suffix)
    if parent_account is not None:
        number_as_char = '{}-{}'.format(parent_account.number_as_char, number_as_char)

    return number_as_char

To be sure that the caching doesn't cause problems you could just clear the cached value after you're done saving: 为确保缓存不会引起问题,您可以在保存完成后清除缓存值:

def save(self, *args, **kwargs):
    self.number_as_char = self.get_number_as_char(
        self.parent_account, self.number_suffix)
    super().save(*args, **kwargs)
    # Clear the cache, in case something edits this object again.
    del self.get_number_as_char

I tinkered with it a bit, and I think I found a better way. 我对此进行了一些修改,我认为我找到了一种更好的方法。

By using the disabled property on the number_as_char field of your model form, you can entirely ignore users input (and make the field disabled in a single step). 通过在模型表单的number_as_char字段上使用Disabled属性,您可以完全忽略用户的输入(并在单个步骤中将该字段禁用)。

Your model already calculates the number_as_char attribute in the save method. 您的模型已经在save方法中计算了number_as_char属性。 However, if the Unique constraint fails, then your admin UI will throw a 500 error. 但是,如果“唯一性”约束失败,那么您的管理界面将引发500错误。 However, you can move your field calculation to the clean() method, leaving the save() method as it is. 但是,您可以将字段计算移至clean()方法,而无需save()方法。

So the full example will look similar to this: 因此,完整的示例将类似于以下内容:

The form: 表格:

class AccountForm(forms.ModelForm):

    class Meta:
        model = Account
        fields = [
            'parent_account', 'number_suffix', 'number_as_char',
        ]
        widgets = {
            'number_as_char': forms.TextInput(attrs={'disabled': True}),
        }

The model: 该模型:

class Account(models.Model):
    # ...

    def clean(self):
        self.number_as_char = self.get_number_as_char(
            self.parent_account, self.number_suffix
        )
        super().clean()

That way anything that generates form based on your model will throw a nice validation error (provided that it uses the built-in model validation, which is the case for Model Forms). 这样,任何基于您的模型生成表单的东西都会抛出一个不错的验证错误(前提是它使用内置的模型验证,对于Model Forms就是这种情况)。

The only downside to this is that if you save a model that triggers the validation error, you will see an empty field instead of the value that failed the validation - but I guess there is some nice way to fix this as well - I'll edit my answer if I also find a solution to this. 唯一的缺点是,如果您保存一个触发验证错误的模型,则会看到一个空字段,而不是验证失败的值-但我想也有一些很好的方法可以解决此问题-我会如果我也找到解决方案,请编辑我的答案。

After reading all the answers and doing some more digging through the docs, I ended up using the following: 阅读所有答案并进一步研究文档后,我最终使用了以下内容:

  1. @samu suggested using the models clean() method and @Laurent S suggested using unique_together for (parent_account, number_suffix) . @samu建议使用模型clean()方法, @Laurent S建议将unique_together用作(parent_account, number_suffix) Since only using unique_together doesn't work for me because parent_account can be null , I opted for combining the two ideas: checking for existing (parent_account, number_suffix) combinations in the models clean() method. 由于仅使用unique_together对我不起作用,因为parent_account可以为null ,所以我选择了结合两种思路:检查模型clean()方法中现有的(parent_account, number_suffix)组合。
  2. As a consecuence, I removed number_as_char from the form and it is now only calculated in the save() method. number_as_char ,我从窗体中删除了number_as_char ,现在仅在save()方法中对其进行计算。 By the way: thanks to @solarissmoke for suggesting to calculated it based on the first parent only, not iterating all the way to the top of the chain. 顺便说一句:感谢@solarissmoke建议仅基于第一个父对象进行计算,而不是一直迭代到链的顶部。
  3. Another consecuence is that I now need to explicitly call the models full_clean() method to validate uniqueness when using the model without the form (otherwise I will get the database IntegrityError ), but I can live with that. 另一个好处是,当使用不带表单的模型时,我现在需要显式调用模型full_clean()方法以验证唯一性(否则我将获得数据库IntegrityError ),但是我可以接受。

So, now my model looks like this: 因此,现在我的模型如下所示:

class Account(models.Model):
    parent_account = models.ForeignKey(
        to='self',
        on_delete=models.PROTECT,
        null=True,
        blank=True)
    number_suffix = models.PositiveIntegerField()
    number_as_char = models.CharField(
        max_length=100,
        default='0',
        unique=True)

    def save(self, *args, **kwargs):
        if self.parent_account is not None:
            self.number_as_char = '{}-{}'.format(
                self.parent_account.number_as_char,
                self.number_suffix)
        else:
            self.number_as_char = str(self.number_suffix)
        super().save(*args, **kwargs)

    def clean(self):
        qs = self._meta.model.objects.exclude(pk=self.pk)
        qs = qs.filter(
            parent_account=self.parent_account,
            number_suffix=self.number_suffix)
        if qs.exists():
            raise ValidationError('... some message ...')

And my form ends up like this: 我的表单最终如下所示:

class AccountForm(forms.ModelForm):
    class Meta:
        model = Account
        fields = [
            'parent_account', 'number_suffix',
        ]

EDIT 编辑

I'll mark my own answer as accepted, because non of the suggestions fully suited my needs. 我会将自己的答案标记为已接受,因为其中没有建议完全符合我的需求。

However, the bounty goes to @samu s answer for pointing me in the right direction with using the clean() method. 但是,赏金会转到@samu的答案,以便使用clean()方法将我指向正确的方向。

Another way - probably not as good though - would be to use Django signals . 另一种方法-可能不太好-将使用Django信号 You could make a pre_save signal that would set the correct value for number_as_char field on the instance that's about to get saved. 您可以发出一个pre_save信号,该信号将为将要保存的实例上的number_as_char字段设置正确的值。

That way you don't have to have it done in a save() method of your model, OR in the clean() method of your ModelForm . 这样,您就不必在模型的save()方法中或在ModelFormclean()方法中完成ModelForm

Using signals should ensure that any operation that uses the ORM to manipulate your data (which, by extend, should mean all ModelForms as well) will trigger your signal. 使用信号应确保使用ORM操纵数据的任何操作(从ModelForms ,也应意味着所有ModelForms )将触发您的信号。

The disadvantage to this approach is that it is not clear from the code directly how is this property generated. 这种方法的缺点是无法直接从代码中了解如何生成此属性。 One has to stumble upon the signal definition in order to discover that it's even there. 人们必须偶然发现信号的定义,以便发现它在那里。 If you can live with it though, I'd go with signals. 如果您可以接受,我会发信号的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM