如何通过 Django field choices 更新数据库?
结论
DetailView 只用于展示对象,不会处理表单提交。要修改 Lead.lead_owner,可以使用 ModelForm 和 UpdateView。
Django 会根据模型字段的 choices 生成 <select>,选中数据库中保存的当前值,并在表单验证通过后更新对应的 Lead 记录。一般不需要在模板中手动维护选项列表。
1. 创建 ModelForm
# forms.py
from django import forms
from .models import Lead
class LeadOwnerUpdateForm(forms.ModelForm):
class Meta:
model = Lead
fields = ["lead_owner"]
lead_owner 定义了 choices,因此 Django 会将它渲染成下拉框。
2. 将 DetailView 改为 UpdateView
# views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse
from django.views.generic import UpdateView
from .forms import LeadOwnerUpdateForm
from .models import Lead
class LeadDetailView(LoginRequiredMixin, UpdateView):
model = Lead
form_class = LeadOwnerUpdateForm
template_name = "lead-details.html"
context_object_name = "lead"
def get_success_url(self):
return reverse("lead-detail", kwargs={"pk": self.object.pk})
UpdateView 会根据 URL 中的 pk 查询 Lead,用当前记录初始化表单,并选中当前的 lead_owner。收到 POST 请求后,它会验证数据并调用 form.save() 更新数据库,保存成功后再跳转回详情页面。
这里建议使用单数形式的 lead 作为上下文变量,因为页面展示的是一条记录,不是记录集合。
如果 agent 只能修改分配给自己的 lead,还需要限制查询范围,例如:
def get_queryset(self):
return Lead.objects.filter(agent__user=self.request.user)
这段代码假设 Agent 模型通过名为 user 的字段关联用户。实际项目中应按具体的模型结构调整。
3. 配置 URL
# urls.py
from django.urls import path
from .views import LeadDetailView
urlpatterns = [
path("leads/<int:pk>/", LeadDetailView.as_view(), name="lead-detail"),
]
访问 /leads/12/ 时,Django 会读取并更新主键为 12 的 Lead。
4. 在模板中显示表单
最简单的方式是直接渲染字段:
<form method="post">
{% csrf_token %}
{{ form.lead_owner.label_tag }}
{{ form.lead_owner }}
{% if form.lead_owner.errors %}
<div class="field-errors">
{{ form.lead_owner.errors }}
</div>
{% endif %}
<button type="submit">保存</button>
</form>
生成的 HTML 大致如下:
<select name="lead_owner" id="id_lead_owner">
<option value="Settlement, New">Settlement, New</option>
<option value="Lead, Sales" selected>Lead, Sales</option>
</select>
Django 会根据当前的 Lead.lead_owner 添加 selected。提交表单后,UpdateView 会将新值保存到数据库。
5. 需要自定义 HTML 时手动遍历 choices
如果需要完全控制 <select> 的结构或 CSS,可以遍历表单字段的 choices:
<form method="post">
{% csrf_token %}
<label for="id_lead_owner">Lead owner</label>
<select name="lead_owner" id="id_lead_owner">
{% for value, label in form.lead_owner.field.choices %}
<option
value="{{ value }}"
{% if form.lead_owner.value == value %}selected{% endif %}
>
{{ label }}
</option>
{% endfor %}
</select>
{% if form.lead_owner.errors %}
{{ form.lead_owner.errors }}
{% endif %}
<button type="submit">保存</button>
</form>
这里应该比较:
form.lead_owner.value
不要只比较:
lead.lead_owner
表单验证失败后,form.lead_owner.value 会保留用户刚提交的值。页面重新显示时,下拉框不会跳回数据库中的旧值。
如果没有特殊的 HTML 结构要求,建议直接使用:
{{ form.lead_owner }}
这样可以少写一些模板逻辑,也能避免选中状态、错误回显和字段属性的处理出现偏差。
6. 关于“动态”的含义
当前模型中的 lead_owner 是带静态 choices 的 CharField:
choices_lead_owner = [
("Settlement, New", "Settlement, New"),
("Lead, Sales", "Lead, Sales"),
]
所以下拉框只会显示这两个选项。Corden, James、Rower, Charles 等人员不会自动出现,因为他们不在 choices_lead_owner 中。
如果 lead owner 指的是数据库中的 agent,更合适的做法是直接关联 Agent,而不是使用 CharField(choices=...):
class Lead(models.Model):
lead_owner = models.ForeignKey(
Agent,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="owned_leads",
)
表单仍然可以使用 ModelForm:
class LeadOwnerUpdateForm(forms.ModelForm):
class Meta:
model = Lead
fields = ["lead_owner"]
此时 Django 会生成 ModelChoiceField,下拉框中的选项来自 Agent 数据库记录,显示文本由 Agent.__str__() 决定:
class Agent(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
def __str__(self):
return f"{self.last_name}, {self.first_name}"
把现有的 lead_owner 从字符串字段改成外键会改变数据库结构,因此需要创建并执行 migration。如果数据库中已经有数据,还要设计数据迁移,把原来的字符串匹配到对应的 Agent 记录。
注意事项
- POST 表单必须包含
{% csrf_token %}。 <select>的name必须是lead_owner,否则表单无法接收这个字段。- 提交值必须是 choices 元组中的第一个元素,例如
"Lead, Sales",不能提交未定义的值。 blank=True表示表单允许留空。如果必须选择 owner,应移除blank=True,并创建 migration。- 不要直接使用
Lead.objects.all()暴露所有记录。CRM 通常需要按当前用户、团队或权限过滤get_queryset(),防止用户修改 URL 中的pk后编辑无权访问的 lead。
备注:内容仅供参考。
