题 验证django admin中的依赖内联


我正在使用Django 1.4,我想设置验证规则来比较不同内联的值。

我有三个简单的课程

在models.py中:

class Shopping(models.Model):
    shop_name = models.CharField(max_length=200)

class Item(models.Model):
    item_name = models.CharField(max_length=200)
    cost = models.IntegerField()
    item_shop = models.ForeignKey(Shopping)

class Buyer(models.Model):
    buyer_name = models.CharField(max_length=200)
    amount = models.IntegerField()
    buyer_shop = models.ForeignKey(Shopping)

在admin.py中:

class ItemInline(admin.TabularInline):
    model = Item

class BuyerInline(admin.TabularInline):
    model = Buyer

class ShoppingAdmin(admin.ModelAdmin):
    inlines = (ItemInline, BuyerInline)

因此,例如,可以以10美元购买一瓶朗姆酒,以8美元购买伏特加酒。迈克支付15美元,汤姆支付3美元。

目标是防止用户使用不匹配的金额保存实例:已支付的金额必须与项目成本的总和相同(即10 + 8 = 15 + 3)。

我试过了:

  • 在Shopping.clean方法中引发ValidationError。但内联尚未更新,因此总和不正确
  • 在ShoppingAdmin.save_related方法中引发ValidationError。但是在这里提出ValidationError会给出一个非常用户不友好的错误页面,而不是重定向到更改页面,并带有一个很好的错误消息。

有没有解决这个问题的方法?客户端(javascript / ajax)验证最简单吗?


13
2017-11-23 10:04


起源


你好,你有没有为此想出一些东西?我面临同样的问题。我能想到的唯一解决方案是内联模型的清理方法,但这会产生很大的数据包开销。 - ppetrid
我想一个解决方案是编辑django admin的行为。查看django / contrib / admin / options.py,add_view方法行924 - Rems


答案:


您可以覆盖内联formset以实现您想要的效果。在formset的clean方法中,您可以通过“instance”成员访问Shopping实例。因此,您可以使用Shopping模型临时存储计算的总数并使您的formset进行通信。在models.py中:

class Shopping(models.Model):
   shop_name = models.CharField(max_length=200)

   def __init__(self, *args, **kwargs)
       super(Shopping, self).__init__(*args, **kwargs)
       self.__total__ = None

在admin.py中:

from django.forms.models import BaseInlineFormSet
class ItemInlineFormSet(BaseInlineFormSet):
   def clean(self):
      super(ItemInlineFormSet, self).clean()
      total = 0
      for form in self.forms:
         if not form.is_valid():
            return #other errors exist, so don't bother
         if form.cleaned_data and not form.cleaned_data.get('DELETE'):
            total += form.cleaned_data['cost']
      self.instance.__total__ = total


class BuyerInlineFormSet(BaseInlineFormSet):
   def clean(self):
      super(BuyerInlineFormSet, self).clean()
      total = 0
      for form in self.forms:
         if not form.is_valid():
            return #other errors exist, so don't bother
         if form.cleaned_data and not form.cleaned_data.get('DELETE'):
            total += form.cleaned_data['cost']

      #compare only if Item inline forms were clean as well
      if self.instance.__total__ is not None and self.instance.__total__ != total:
         raise ValidationError('Oops!')

class ItemInline(admin.TabularInline):
   model = Item
   formset = ItemInlineFormSet

class BuyerInline(admin.TabularInline):
   model = Buyer
   formset = BuyerInlineFormSet

这是你可以做到的唯一干净的方式(据我所知),一切都放在应有的位置。

编辑: 添加了* if form.cleaned_data *检查,因为表单也包含空内联。 请告诉我这对您有何帮助!

EDIT2: 添加了对要删除的表单的检查,正如评论中正确指出的那样。这些表格不应参与计算。


25
2017-12-25 13:40



真棒!遗憾的是我不能投票给你答案;我没有足够的声誉。编辑:NVM一些声望点神奇地出现了 - Rems
它应该忽略已删除的行: if form.cleaned_data.get('DELETE'): continue - Rune Kaagaard
这是一个可爱的策略,谢谢。但是,我有一个问题,因为当没有添加内联时,不会出现错误消息。在我的代码中,我只定义了一个内联formset,因为我将它与主模型中的字段进行比较(所以在上面的例子中, BuyerInlineFormSet,我会用这个比较 if self.instance.amount != total: raise ...。当我保存 Shopping 金额> 0且不添加任何金额的实例 Buyers,它告诉我表格是有效的,即使它不是(因为没有买方金额的总和是0)。 - jenniwren
这太棒了。谢谢,我认为在查看其他地方之后这是不可能的。但是,值得注意的是,只有这样才能有效 ItemInline 出现在之前 BuyerInline 在里面 inlines 的清单 ShoppingAdmin,即 clean() 首先出现的Inline的方法将首先运行,这依赖于此行为。 - yerforkferchips


好吧,我有一个解决方案。它涉及编辑django管理员的代码。

在django / contrib / admin / options.py中,在add_view(第924行)和change_view(第1012行)方法中,找到这部分:

        [...]
        if all_valid(formsets) and form_validated:
            self.save_model(request, new_object, form, True)
        [...]

并替换它

        if not hasattr(self, 'clean_formsets') or self.clean_formsets(form, formsets):
            if all_valid(formsets) and form_validated:
                self.save_model(request, new_object, form, True)

现在,在您的ModelAdmin中,您可以执行类似的操作

class ShoppingAdmin(admin.ModelAdmin):
    inlines = (ItemInline, BuyerInline)
    def clean_formsets(self, form, formsets):
        items_total = 0
        buyers_total = 0
        for formset in formsets:
            if formset.is_valid():
                if issubclass(formset.model, Item):
                    items_total += formset.cleaned_data[0]['cost']
                if issubclass(formset.model, Buyer):
                    buyers_total += formset.cleaned_data[0]['amount']

        if items_total != buyers_total:
            # This is the most ugly part :(
            if not form._errors.has_key(forms.forms.NON_FIELD_ERRORS):
                form._errors[forms.forms.NON_FIELD_ERRORS] = []
            form._errors[forms.forms.NON_FIELD_ERRORS].append('The totals don\'t match!')
            return False
        return True

这不仅仅是一个适当的解决方案。任何改进建议?有谁认为这应该是django上的功能请求?


-2
2017-12-22 14:40



这确实是一个黑客攻击,因为我们必须手动将错误添加到列表中而不是引发ValidationError。但它仍然有效!我认为这基本上是一个formset验证的问题。从这个意义上讲,也许可以创建一个自定义的FormSet类,实现一个正确的clean方法,并使用该类而不是内联中的默认formset。只是一个想法.. - ppetrid
你建议手动创建一个FormSet吗?所以基本上没有内联,你必须手工处理相关的保存,没有“添加另一个按钮”等...你只是松散内联的所有力量:( - Rems
对不起,也许我不清楚,我建议覆盖内联表格。自从我为自己的项目提出解决方案后,我最终发布了一个单独的答案。 - ppetrid
更改Django代码几乎不是解决方案:您现在已经绑定到该特定版本,并且记得在您决定升级时应用这些更改。这就像分叉Django项目...... - Don