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

2024-04-26 10:27:34 发布

您现在位置:Python中文网/ 问答频道 /正文

TL;DR我的模型和表单都计算字段number_as_char的值。我可以避免重复工作,但在使用没有表单的模型时仍然检查唯一性吗?在

我使用python3和django1.11


我的模型如下所示:

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)

字段number_as_char不应由用户设置,因为它是基于选定的parent_account计算的:它是通过链接所有父帐户和当前实例的字段number_suffix的值来获得的。在

下面是一个有三个帐户的示例:

^{pr2}$

{/strong>要确保字段的唯一性,而不需要使用字段的唯一性。在


我的表格如下:

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'])

我将number_as_char包含在具有widget属性readonly的表单中,并使用formsclean()方法来计算number_as_char(必须在验证唯一性之前计算它)。在


这一切都可以(模型和表单),但是在验证表单之后,number_as_char的值将通过模型save()方法再次计算。这不是什么大问题,但有没有办法避免这种双重计算?在

  1. 如果我从formsclean()方法中删除计算,那么新值将不会验证唯一性(它只检查旧值)。在
  2. 我不想完全从模型中删除计算,因为我在其他部分使用模型而不使用表单。在

你有什么建议可以用不同的方法来避免重复计算字段吗?在


Tags: 方法模型selftrue表单numbergetmodels
3条回答

我做了一点修补,我想我找到了一个更好的方法。在

通过对模型表单的number_as_char字段使用disabled属性,可以完全忽略用户的输入(并在一个步骤中禁用该字段)。在

您的模型已经在save方法中计算了number_as_char属性。但是,如果Unique约束失败,那么您的管理UI将抛出500个错误。但是,您可以将字段计算移动到clean()方法,而保留{}方法。在

因此,完整的示例将与此类似:

表格:

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}),
        }

模型:

^{pr2}$

这样,基于模型生成表单的任何东西都会抛出一个很好的验证错误(前提是它使用内置的模型验证,模型表单就是这种情况)。在

唯一的缺点是,如果您保存了一个触发验证错误的模型,您将看到一个空字段,而不是验证失败的值-但是我想有一些很好的方法来解决这个问题-如果我也找到了解决方法,我会编辑我的答案。在

我看不出有任何方法可以在两个地方做到这一点(save()clean()),因为您也需要它来处理基于非表单的存储)。在

不过,我可以为您的get_number_as_char方法提供两个效率改进:

  1. 将其设为^{},以便第二次调用它时,只需返回一个缓存值并消除双重计算。显然,您需要注意的是,在更新实例之前不要调用,否则旧的number_as_char将被缓存。只要get_number_as_char()只在保存/清理期间调用,就可以了。

  2. 根据上面提供的信息,您不必迭代所有祖先,而可以简单地将number_as_char作为父元素并附加到它上面。

以下内容包括:

@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

为了确保缓存不会导致问题,您可以在保存完成后清除缓存的值:

^{pr2}$

在阅读了所有答案并对文档进行了进一步的挖掘之后,我最终使用了以下方法:

  1. @samu建议使用模型clean()方法,@Laurent S建议使用unique_together作为(parent_account, number_suffix)。由于仅使用unique_together对我来说不起作用,因为parent_account可以是{},所以我选择了结合这两个想法:检查模型clean()方法中现有的(parent_account, number_suffix)组合。在
  2. 作为一个连续,我从表单中删除了number_as_char,现在它只在save()方法中计算。顺便说一句:感谢@solarissmoke建议只根据第一个父级计算它,而不是一直迭代到链的顶部。在
  3. 另一个连续性是,我现在需要显式地调用modelsfull_clean()方法来验证不带表单的模型的唯一性(否则我将得到数据库IntegrityError),但我可以接受这一点。在

所以,现在我的模型是这样的:

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

我的状态是这样的:

^{pr2}$

编辑

我会把我自己的答案标记为接受,因为没有完全适合我需要的建议。在

但是,@samu的回答让我用clean()的方法指引了我的方向。在

相关问题 更多 >