从属或链接下拉列表是一个特殊字段,它依赖于先前选择的字段,以显示已过滤选项的列表。最常见的地方就是通过地址里选择省份,然后显示出该省份下所有城市列表。
从属或链接下拉列表是一个特殊字段,它依赖于先前选择的字段,以显示已过滤选项的列表。最常见的地方就是通过地址里选择省份,然后显示出该省份下所有城市列表。
以下面的应用程序为例:
models.py
from django.db import models
class Country(models.Model):
name = models.CharField(max_length=30)
def __str__(self):
return self.name
class City(models.Model):
country = models.ForeignKey(Country, on_delete=models.CASCADE)
name = models.CharField(max_length=30)
def __str__(self):
return self.name
class Person(models.Model):
name = models.CharField(max_length=100)
birthdate = models.DateField(null=True, blank=True)
country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True)
city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True)
def __str__(self):
return self.name
在应用程序中,我们将创建一个简单的表单处理来创建和更新人物对象。这个联动列表用来选择人物的国家和所在城市。
urls.py
from django.urls import include, path
from . import views
urlpatterns = [
path('', views.PersonListView.as_view(), name='person_changelist'),
path('add/', views.PersonCreateView.as_view(), name='person_add'),
path('<int:pk>/', views.PersonUpdateView.as_view(), name='person_change'),
]
创建三个视图函数:
views.py
from django.views.generic import ListView, CreateView, UpdateView
from django.urls import reverse_lazy
from .models import Person
class PersonListView(ListView):
model = Person
context_object_name = 'people'
class PersonCreateView(CreateView):
model = Person
fields = ('name', 'birthdate', 'country', 'city')
success_url = reverse_lazy('person_changelist')
class PersonUpdateView(UpdateView):
model = Person
fields = ('name', 'birthdate', 'country', 'city')
success_url = reverse_lazy('person_changelist')
我们运行它,但是发现这个功能和我们想要要的不太一样,我们是想要根据选择国家,然后显示出该国家下的所有城市。但现在程序把所有国家的城市都给列出来了。
用一个简单的HTML代码把效果显示出来:
{% extends 'base.html' %}
{% block content %}
<h2>Person Form</h2>
<form method="post" novalidate>
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<button type="submit">Save</button>
<a href="{% url 'person_changelist' %}">Nevermind</a>
</form>
{% endblock %}
实现它的最佳方法是创建一个模型表单。通过这种方式,我们可以非常灵活地实现我们想要的功能。
forms.py
from django import forms
from .models import Person, City
class PersonForm(forms.ModelForm):
class Meta:
model = Person
fields = ('name', 'birthdate', 'country', 'city')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['city'].queryset = City.objects.none()
上面的示例有一个比较重要细的地方,我们简单的定义表单:现我们将覆盖默认的init方法,并将city字段的queryset设置为空的列表:
PS:不要忘记修改视图定义,使用我们的新表单类:
views.py
class PersonCreateView(CreateView):
model = Person
form_class = PersonForm
success_url = reverse_lazy('person_changelist')
class PersonUpdateView(UpdateView):
model = Person
form_class = PersonForm
success_url = reverse_lazy('person_changelist')
现在我们需要创建一个视图来返回给定国家/地区的城市列表。该视图将通过使用AJAX进行请求。
views.py
def load_cities(request):
country_id = request.GET.get('country')
cities = City.objects.filter(country_id=country_id).order_by('name')
return render(request, 'hr/city_dropdown_list_options.html', {'cities': cities})
用这样简单的基于函数的视图就可以非常好的实现。下面是我们的HTML模板:
templates/hr/city_dropdown_list_options.html
<option value="">---------</option>
{% for city in cities %}
<option value="{{ city.pk }}">{{ city.name }}</option>
{% endfor %}
然后我们给之前的视图函数创建一个URL:
urls.py
from django.urls import include, path
from . import views
urlpatterns = [
path('', views.PersonListView.as_view(), name='person_changelist'),
path('add/', views.PersonCreateView.as_view(), name='person_add'),
path('<int:pk>/', views.PersonUpdateView.as_view(), name='person_change'),
path('ajax/load-cities/', views.load_cities, name='ajax_load_cities'), # <--新增这个
]
现在建一个AJAX请求。在下面的示例中,我使用的是jQuery,但可以使用任何JavaScript框架(或只是简单的JavaScript)来创建异步请求:
templates/person_form.html
{% extends 'base.html' %}
{% block content %}
<h2>Person Form</h2>
<form method="post" id="personForm" data-cities-url="{% url 'ajax_load_cities' %}" novalidate>
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<button type="submit">Save</button>
<a href="{% url 'person_changelist' %}">Nevermind</a>
</form>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script>
$("#id_country").change(function () {
var url = $("#personForm").attr("data-cities-url"); // 获取 `load_cities` view
var countryId = $(this).val(); // 在HTML中获取选取的国家ID
$.ajax({ // 初始化 AJAX 请求
url: url, //设置请求地址(= localhost:8000/hr/ajax/load-cities/)
data: {
'country': countryId // countryId传到GET参数
},
success: function (data) { // 视图函数 `load_cities`返回到`data`
$("#id_city").html(data); // 用返回的数据替换城市输入框的内容
}
});
});
</script>
{% endblock %}
首先,我为表单(personForm)添加了一个ID,以便我们可以更容易地访问它。之后,我将数据属性添加到专用章data-cities-url。
然后,在此之后,我们在国家/地区下拉列表中有一个监听器,设置id_country标识。此ID由Django自动生成。当它发生变化时,它会向服务器发出一个AJAX请求,并将所选的国家ID传递给我们的视图。
请求成功后,我们的脚本将在cities下拉列表中添加由load_cities视图呈现的HTML,由HTML ID id_city标识。
现在前端已经做好了,但是后端并没有按预期工作。如果我们现在提交表单,我们将看到以下错误消息:
那是因为我们在表单定义中列出了空的城市列表。这个错误消息是我特意向你显示的,因为它实际上非常有用。它将帮助我们保持形式的一致性。这意味着Django表单检查查询集中是否存在提供的值。
修改一下代码:
forms.py
from django import forms
from .models import Person, City
class PersonForm(forms.ModelForm):
class Meta:
model = Person
fields = ('name', 'birthdate', 'country', 'city')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['city'].queryset = City.objects.none()
if 'country' in self.data:
try:
country_id = int(self.data.get('country'))
self.fields['city'].queryset = City.objects.filter(country_id=country_id).order_by('name')
except (ValueError, TypeError):
pass # invalid input from the client; ignore and fallback to empty City queryset
elif self.instance.pk:
self.fields['city'].queryset = self.instance.country.city_set.order_by('name')
这个表单将提供一个非常好的行为。如果有表单POST数据(数据不是无),它将使用收到的表单的国家ID加载城市列表。如果它是无效输入,只需丢弃它,表单将为用户提示错误信息。如果没有POST数据但表单中有实例(意味着正在使用该表单更新现有人员),请使用所选国家/地区的城市列表。如果没有,只需返回一个空的城市列表,因为它是一个全新的形式(self.fields ['city']已经设置为空的查询集,还记得吗?)。
或者,您可以从表单定义中完全删除country字段,因为它与城市无关。但我更喜欢将它保留在那里,因为在某些情况下您需要保存这两个值,并且在上面的示例中,您可以了解如何实现它而不必干扰表单的呈现过程。
发表评论