基于用户权限的自定义Django inlineformset验证

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

enter image description here

如果所有值都正确输入,则初始表单将正确显示并能够正确保存输入 - 但是,如果未通过验证,则不正确的字段将被突出显示并清除其值。

enter image description here

models.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

views.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()方法 - 然而,我无法确定提交的表单是否被禁用。

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

覆盖 BaseInlineFormSet 的 clean() 方法,但是我无法在 init 中禁用字段或任何验证规则(它会在验证失败时悄无声息地失败,并呈现一个空的 inlineformset - 它永远不会到达 clean() 方法)。

forms.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)被禁用了。两个批准复选框行中的一个应该是可编辑的。

enter image description here


有趣的问题。只是好奇,用户需要查看多少个表单集中的行?只是想知道,因为如果一次只有几个,您可以通过循环常规表单类获得各种细粒度的控制和自定义。缺点是您可能需要逐个保存每行。再次强调,这取决于数量。但是,对于表单集总体而言,如果您想对某一行执行任何操作,则需要在循环中处理它:for form in formset:#do something - Milo Persic
嗨Milo - 感谢您的回复。关于行数 - 它是动态的(它取决于原始订单中输入了多少行)。因此,它可能是10或500。至于您关于在表单集中循环表单的建议 - 您会将此代码放在哪里? - dj.bettega
视图需要一个函数来生成一系列表单,每个表单都有自己的对象实例。模板将会解包它。基于你所处理的对象数量,我不确定我是否会选择这种方法。Formsets 可能是一个很好的解决方案,以及自定义验证。作为第一步,我建议在你的模型字段(如“quantity”、“product”等)中添加 blank=True 和/或 null=True。这就是为什么表单显示“必填项”,而没有提交的原因。然后在模型表单类中处理所需或不需要的内容。 - Milo Persic
顺便问一下,你有没有使用ajax刷新表单并显示验证器中的错误?如果没有,我认为这可能是你最需要的部分。 - Milo Persic
嗨Milo - 验证在“budgetholder_approved”字段上失败,而模型允许空值。因此,由模型定义的验证不应失败(现有行的所有其他字段已存在,因此这些也不应失败)。无论如何 - 在我尝试在__init__中覆盖formset时,这应该定义验证规则。我目前避免使用ajax,因为我正在尝试在改善UX之前在后端进行验证。 - dj.bettega
1个回答

0

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

        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

针对更新的问题进行编辑: 由于时间限制,我没有测试过这个,但是因为您已经初始化了字段并覆盖了小部件,所以如果您需要更新小部件的disabled属性而不是字段。

self.fields['budgetholder_approved'].widget = forms.RadioSelect(choices=YES_OR_NO)
self.fields['budgetholder_approved'].widget.attrs['disabled'] = False

嗨道 - 抱歉回复晚了。我记不起来为什么我这样做了。最后我觉得这并没有改变任何事情,因为"self.object"仍然会从get_object(self)方法中返回self.activation.process.order。我对实例的理解是,它指的是底层记录(在这种情况下是订单),而不一定是表单实例。这会改变行为吗? - dj.bettega
我是个白痴!您的纠正确实有助于解决最初的验证问题。但仍有一个问题未得到解决。当带有验证的表单重新加载时,应该启用的字段(budgetholder_approved)被禁用了。我更新了问题以说明。 - dj.bettega

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接