Django - 对模型中的计算字段以及ModelForm

时间:2018-05-04 16:10:31

标签: python django

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

我使用Python 3和Django 1.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的值获得的父帐户和当前实例。

以下是三个帐户的示例:

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'

NOT 是删除字段并使用模型属性的选项,因为我需要确保唯一性,并使用该字段对order_by()的查询集进行排序。

我的表格如下:

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中包含readonly,我使用窗体clean()方法计算number_as_char(必须在验证唯一性之前计算)。

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

  1. 如果我从表单clean()方法中删除计算,则不会使用新值验证唯一性(它只会检查旧值)。
  2. 我不想完全从模型中删除计算,因为我在没有表单的情况下在其他部分使用该模型。
  3. 您是否有任何建议可以采取不同的措施以避免对该字段进行双重计算?

4 个答案:

答案 0 :(得分:1)

我无法在两个地方(save()clean())看到这样做,因为您需要它来处理非基于表单的保存)。

但是,我可以为您的get_number_as_char方法提供两项效率改进:

  1. 设为cached_property,以便第二次调用它时,只需返回一个缓存值并消除双重计算。显然,您需要注意,在实例更新之前不会将其称为,否则旧的number_as_char将被缓存。只要在保存/清理期间调用get_number_as_char(),这应该没问题。

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

    < / LI>

    以下内容包括:

    @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
    

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

    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
    

答案 1 :(得分:1)

我修了一下,我觉得我找到了更好的方法。

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

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

所以完整的例子看起来与此相似:

表格:

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

模特:

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

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

这样,根据您的模型生成表单的任何内容都会抛出一个很好的验证错误(前提是它使用内置模型验证,这是模型表单的情况)。

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

答案 2 :(得分:1)

在阅读完所有答案并进行更多挖掘文档之后,我最终使用了以下内容:

  1. @samu建议使用模型clean()方法和@Laurent S建议使用unique_together (parent_account, number_suffix)。由于仅使用unique_together对我不起作用,因为parent_account可以是null,因此我选择合并这两个提示:检查模型中(parent_account, number_suffix)个现有组合{{ 1}}方法。
  2. 作为结果,我从表单中删除了clean(),现在只能使用number_as_char方法进行计算。顺便说一句:感谢save()建议仅根据第一个父项计算它,而不是一直迭代到链的顶部。
  3. 另一个结果是我现在需要显式调用模型@solarissmoke方法来验证在没有表单时使用模型的唯一性(否则我将得到数据库full_clean()),但我可以忍受这一点。
  4. 所以,现在我的模型看起来像这样:

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

    修改

    我会将自己的答案标记为已接受,因为这些建议完全不符合我的需求。

    然而,赏金转到class AccountForm(forms.ModelForm): class Meta: model = Account fields = [ 'parent_account', 'number_suffix', ] 的答案,指出我使用@samu方法指向正确的方向。

答案 3 :(得分:0)

另一种方式 - 可能不太好 - 会使用Django signals。您可以发出pre_save信号,为即将保存的实例上的number_as_char字段设置正确的值。

通过这种方式,您不必使用模型的save()方法或clean() ModelForm方法完成此操作。

使用信号应确保使用ORM操作数据的任何操作(通过扩展,也应该表示所有ModelForms)将触发您的信号。

这种方法的缺点是直接从代码中不清楚这个属性是如何生成的。人们不得不偶然发现信号定义,以便发现它甚至存在。如果你能忍受它,我会选择信号。