Django forms 报错:'Event' is not iterable
结论
报错出在这一行:
form = PaymentForm(event, athlete)
PaymentForm 继承自 forms.Form。如果没有自定义 __init__(),Django 会把第一个位置参数当作表单的 data,第二个位置参数当作 files。这样一来,event 就被误当成了类似 request.POST 的字典。
渲染 {{ form }} 时,Django 会尝试从 event 中读取字段,并执行类似下面的判断:
if name not in data:
但 Event 实例不是可迭代的表单数据,因此会报错:
TypeError: argument of type 'Event' is not iterable
解决方法是把 event 和 athlete 定义为 PaymentForm 的自定义关键字参数,并在初始化表单时从 kwargs 中取出。
正确初始化 PaymentForm
表单可以这样定义:
from django import forms
class PaymentForm(forms.Form):
card_number = forms.CharField()
amount = forms.DecimalField(
max_digits=10,
decimal_places=2,
widget=forms.HiddenInput()
)
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
self.athlete = kwargs.pop('athlete')
super(PaymentForm, self).__init__(*args, **kwargs)
# 根据 event 和 athlete 查询实际费用
# fee = RegistrationFee.objects.get(
# event=self.event,
# athlete=self.athlete
# )
# self.fields['amount'].initial = fee.amount
自定义参数必须先通过 pop() 取出,然后再调用父类的 __init__(),因为 Django 的基础表单类不认识 event 和 athlete 这两个参数。
如果它们不是每次都必须提供,可以写成:
self.event = kwargs.pop('event', None)
self.athlete = kwargs.pop('athlete', None)
修改 payment view
GET 和 POST 请求都要传入相同的上下文参数:
from django.shortcuts import get_object_or_404, redirect, render
def payment(request, event_year, event_slug):
event = get_object_or_404(
Event,
event_date__year=event_year,
event_slug=event_slug
)
athlete = get_object_or_404(
Athlete,
email_address=request.session.get('user_email')
)
if request.method == 'POST':
form = PaymentForm(
request.POST,
event=event,
athlete=athlete
)
if form.is_valid():
# 使用 form.cleaned_data 处理支付
payment_data = form.cleaned_data
# process payment_data...
return redirect('home')
else:
form = PaymentForm(
event=event,
athlete=athlete
)
return render(request, 'events/event_payment.html', {
'event': event,
'athlete': athlete,
'form': form,
})
这里应该写成:
PaymentForm(event=event, athlete=athlete)
不能写成:
PaymentForm(event, athlete)
处理 POST 请求时,request.POST 仍作为表单数据传入:
PaymentForm(request.POST, event=event, athlete=athlete)
如果只是设置初始值
如果表单不需要保存 event 和 athlete,只需要在字段中显示计算出的费用,也可以直接使用 initial:
form = PaymentForm(initial={
'amount': registration_fee,
})
处理 POST 请求时,照常绑定用户提交的数据:
form = PaymentForm(request.POST)
但不能只相信浏览器提交的 amount。隐藏字段也可以被用户修改,因此在支付前,服务端应根据 event 和 athlete 重新查询或计算最终费用。
普通 Form 没有 save()
题目中的 PaymentForm 是普通 forms.Form,不是 ModelForm,所以默认没有:
form.save()
验证通过后,应读取:
form.cleaned_data
再自行创建数据库记录或调用支付逻辑,例如:
if form.is_valid():
payment = Payment.objects.create(
athlete=athlete,
event=event,
amount=calculated_fee
)
除非在 PaymentForm 中自行实现了 save() 方法,否则不能调用 form.save()。
Template 还需要 form 标签
当前模板中只有 {% csrf_token %} 和 {{ form }},没有实际的 HTML <form> 标签。可以改成:
{% extends "base.html" %}
{% block head %}
{{ event.event_date.year }} {{ event.event_name }} Payment
{% endblock head %}
{% block content %}
<h1>
{{ event.event_date.year }} {{ event.event_name }}
Entry Form for {{ athlete.first_name }} {{ athlete.last_name }}
</h1>
<form method="post">
{% csrf_token %}
<p>
Payment for {{ athlete.first_name }} {{ athlete.last_name }}
</p>
{{ form.as_p }}
<button type="submit">Register</button>
</form>
{% endblock content %}
{{ form }} 只负责渲染表单字段,不会自动生成 <form> 标签、提交方式和提交按钮。
其他注意事项
如果 Event.objects.get() 找不到记录,会直接抛出 Event.DoesNotExist。因此后面再写:
if not event:
raise Http404
不会生效。这里更适合使用 get_object_or_404()。
session 中也可能没有 user_email。生产环境中最好先检查这个值:
email = request.session.get('user_email')
if not email:
return redirect('register', event_year=event_year, event_slug=event_slug)
问题不在模板本身。{{ form }} 在渲染表单时触发了相关处理流程,才暴露出初始化方式有误。将 event 和 athlete 改为明确的自定义关键字参数,并按普通 Form 的方式处理验证结果,就能解决这个问题。
备注:内容仅供参考。
