【问题标题】:Custom django inlineformset validation based on user permissions基于用户权限的自定义 django inlineformset 验证
【发布时间】:2023-02-04 16:40:05
【问题描述】:

目标是拥有一个简单的工作流,其中订单和相关订单行(在上一步中创建)需要得到相关预算持有人的批准。审批表显示所有订单行,但禁用当前用户未关联的那些行(他们应该能够看到整个订单,但只能编辑他们被允许的行)。如有必要,他们应该能够添加新行。用户需要决定是否批准(批准单选不能为空)

初始表单正确显示并且能够在正确输入所有值时正确保存输入 - 但是,如果验证失败,则不正确的字段会突出显示并清除它们的值。

模型.py

class Order(models.Model):
    department = models.ForeignKey(user_models.Department, on_delete=models.CASCADE)
    location = models.ForeignKey(location_models.Location, on_delete=models.CASCADE, null=True)
    description = models.CharField(max_length=30)
    project = models.ForeignKey(project_models.Project, on_delete=models.CASCADE)
    product = models.ManyToManyField(catalogue_models.Product, through='OrderLine', related_name='orderlines')
    total = models.DecimalField(max_digits=20, decimal_places=2, null=True, blank=True)

    def __str__(self):
        return self.description

class OrderLine(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE)
    project_line = models.ForeignKey(project_models.ProjectLine, on_delete=models.SET_NULL, null=True, blank=False)
    product = models.ForeignKey(catalogue_models.Product, on_delete=models.CASCADE)
    quantity = models.PositiveIntegerField()
    price = models.DecimalField(max_digits=20, decimal_places=4)
    total = models.DecimalField(max_digits=20, decimal_places=2)
    budgetholder_approved = models.BooleanField(null=True)

    def get_line_total(self):
        total = self.quantity * self.price
        return total

    def save(self, *args, **kwargs):
        self.total = self.get_line_total()
        super(OrderLine, self).save(*args, **kwargs)

    def __str__(self):
        return self.product.name

视图.py

class BudgetApprovalView(FlowMixin, generic.UpdateView):
    form_class = forms.BudgetHolderApproval

    def get_object(self):
        return self.activation.process.order

    def get_context_data(self, **kwargs):
        data = super(BudgetApprovalView, self).get_context_data(**kwargs)

        if self.request.POST:
            data['formset'] = forms.OrderLineFormet(self.request.POST, instance=self.object)
        else:
            data['formset'] = forms.OrderLineFormet(instance=self.activation.process.order, form_kwargs={'user': self.request.user})
        return data


    def post(self, request, *args, **kwargs):

        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        form = forms.BudgetHolderApproval(self.request.POST, instance=self.activation.process.order)
        formset = forms.OrderLineFormet(self.request.POST, instance=self.activation.process.order)

        if form.is_valid() and formset.is_valid():
            return self.is_valid(form, formset)

        else:
            return self.is_invalid(form, formset)

    def is_valid(self, form, formset):

        self.object = form.save(commit=False)
        self.object.created_by = self.request.user
        self.activation.process.order = self.object


        with transaction.atomic():
            self.object.save()
            self.activation.done()
            formset.save()

        return HttpResponseRedirect(self.get_success_url())

    def is_invalid(self, form, formset):

        return self.render_to_response(self.get_context_data(form=form, formset=formset))

我已经尝试了几件事来解决这个问题 - 但没有成功:

  1. 覆盖 ModelForm 的 clean() 方法 - 但是,我不知道如何确定提交的表单是否被禁用。

    表单.py

    class OrderForm(forms.ModelForm):
        class Meta:
            model = models.Order
            fields = ['description', 'project', 'location']
    
        def __init__(self, *args, **kwargs):
            super(OrderForm, self).__init__(*args, **kwargs)
    
            self.helper = FormHelper()
            self.helper.form_tag = False
    
    
    class OrderLine(forms.ModelForm):
        class Meta:
            model = models.OrderLine
            exclude = ['viewflow']
    
        def __init__(self, *args, **kwargs):
    
            YES_OR_NO = (
                (True, 'Yes'),
                (False, 'No')
            )
    
            self.user = kwargs.pop('user', None)
    
            super(OrderLine, self).__init__(*args, **kwargs)
    
            self.fields['project_line'].queryset = project_models.ProjectLine.objects.none()
            self.fields['budgetholder_approved'].widget = forms.RadioSelect(choices=YES_OR_NO)
    
            if self.instance.pk:
                self.fields['budgetholder_approved'].required = True
                self.fields['order'].disabled = True
                self.fields['project_line'].disabled = True
                self.fields['product'].disabled = True
                self.fields['quantity'].disabled = True
                self.fields['price'].disabled = True
                self.fields['total'].disabled = True
                self.fields['budgetholder_approved'].disabled = True
    
            if 'project' in self.data:
                try:
                    project_id = int(self.data.get('project'))
                    self.fields['project_line'].queryset = project_models.ProjectLine.objects.filter(project_id=project_id)
                except (ValueError, TypeError):
                    pass
            elif self.instance.pk:
                self.fields['project_line'].queryset = self.instance.order.project.projectline_set
                project_line_id = int(self.instance.project_line.budget_holder.id)
                user_id = int(self.user.id)
    
                if project_line_id == user_id:
                    self.fields['budgetholder_approved'].disabled = False
    
    
            self.helper = FormHelper()
            self.helper.template = 'crispy_forms/templates/bootstrap4/table_inline_formset.html'
            self.helper.form_tag = False
    
        def clean(self):
    
            super(OrderLine, self).clean()
    
            pprint(vars(self.instance))
            
            //This just returns a list of fields without any attributes to apply the validation logic
    
    
    OrderLineFormet = forms.inlineformset_factory(
        parent_model=models.Order,
        model=models.OrderLine,
        form=OrderLine,
        extra=2,
        min_num=1
    )
    
    1. 覆盖 BaseInlineFormSet 的 clean() 方法 - 但是,我无法禁用在里面或任何验证规则(它默默地验证失败并在失败时显示一个空白的内联表单集 - 它永远不会进入 clean() 方法。

    表单.py

    class OrderForm(forms.ModelForm):
        class Meta:
            model = models.Order
            fields = ['description', 'project', 'location']
    
        def __init__(self, *args, **kwargs):
            super(TestOrderForm, self).__init__(*args, **kwargs)
    
            self.helper = FormHelper()
            self.helper.form_tag = False
    
    
    class BaseTestOrderLine(forms.BaseInlineFormSet):
        def __init__(self, user, *args, **kwargs):
            self.user = user
    
            super(BaseTestOrderLine, self).__init__(*args, **kwargs)
    
            self.helper = FormHelper()
            self.helper.template = 'crispy_forms/templates/bootstrap4/table_inline_formset.html'
            self.helper.form_tag = False
            
        // Never gets to the clean method as is_valid fails silently
    
        def clean(self):
            super(BaseTestOrderLine, self).clean()
    
            if any(self.errors):
    
                pprint(vars(self.errors))
    
                return
                
    OrderLineFormet = forms.inlineformset_factory(
        parent_model=models.Order,
        model=models.OrderLine,
        formset=BaseTestOrderLine,
        exclude=['order'],
        extra=2,
        min_num=1
    )
    

    编辑- 根据 Dao 的建议反映进度(表单正确重新加载并正确显示验证错误)

    唯一剩下的问题是,当表单重新加载时 - 仍应启用的字段 (budgetholder_approved) 被禁用。两个批准复选框行之一应该是可编辑的

【问题讨论】:

  • 有趣的问题。只是好奇,用户需要查看表单集中的多少行?只是想知道,因为如果一次只有几个,您可以通过循环常规表单类来获得各种粒度控制和自定义。缺点是您可能必须单独保存每一行。同样,取决于数量。但是对于一般的表单集,如果你想对一行做任何事情,你需要在一个循环中处理它:for form in formset: # do something
  • 嗨 Milo - 感谢您的回复。就行数而言 - 它是动态的(它取决于原始顺序中输入的行数。所以它可能是 10 或 500。至于你关于在表单集中循环表单的建议 - 你会把这段代码放在哪里?
  • 视图需要一个函数来生成表单列表,每个表单都有自己的对象实例。该模板将解压它。根据您处理的对象数量,我不确定我会走那条路。表单集和自定义验证可能是一个很好的解决方案。作为第一步,我会考虑将 blank=True 和/或 null=True 添加到您的模型字段,如“数量”、“产品”等。这就是表单显示“必填”而不是提交的原因。然后处理模型表单类上需要或不需要的内容。
  • 顺便问一下,您是否有任何 ajax 可以刷新表单并显示来自验证器的错误?如果是这样的话,我认为这可能是您的主要缺失部分。
  • 嗨 Milo - 在模型允许空值的“budgetholder_approved”字段上验证失败。因此模型定义的验证不应失败(现有行的所有其他字段已经存在,因此这些也不应失败)。无论如何 - 我试图覆盖中的表单集在里面这应该定义验证规则。我在这一点上避免使用 ajax,因为我试图在改进用户体验之前在后端进行验证

标签: python django validation django-forms inline-formset


【解决方案1】:

看起来是因为您在提交无效时有不同的表单集上下文数据

        if self.request.POST:
            data['formset'] = forms.OrderLineFormet(self.request.POST, instance=self.activation.process.order, form_kwargs={'user': self.request.user})
        else:
            data['formset'] = forms.OrderLineFormet(instance=self.activation.process.order, form_kwargs={'user': self.request.user})
        return data

您可以尝试使用相同的表单数据吗

【讨论】:

  • 嗨 Dao - 抱歉我回复晚了。我不记得我为什么这样做了。最后我认为这不会改变任何东西,因为“self.object”仍然会从 get_object(self) 方法返回 self.activation.process.order。我对该实例的理解是,这是指基础记录(在本例中为订单)——不一定是表单实例。这会改变行为吗?
  • 我是一个白痴!您的更正确实有助于解决初始验证问题。有一个问题仍然存在。当重新加载带有验证的表单时 - 应启用的字段 (budgetholder_approved) 将重新加载为禁用状态。我更新了问题以说明
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-07-16
  • 1970-01-01
  • 2021-05-15
  • 2014-01-10
  • 1970-01-01
  • 2012-12-19
  • 2012-02-25
相关资源
最近更新 更多