最近在python的邮件列表上看到有人问django如何实现多级联动菜单,我自己在做的一个项目也需要这个功能,但是找了半天也没有现成的解决方案,只好自己实现了一个。
由于我对JavaScript不是很熟,所以采用了现成的Ajax框架,粗略比较了一下,选择了比较 Pythonic 的 MochiKit。
Django没有绑定特定的Ajax框架有好有坏,好的方面,我们可以选择自己熟悉和喜欢的框架,坏的的方面,要和后台应用集成部分的工作就得全部自己来做了。
废话不多说了,直接贴代码和说下基本原理。由于大部分代码直接抽取自我现在在做的项目,所以可扩展性还没有到达很好的程度,但是如果理解的话还是可以很容易适应各种情况的需求。
所谓多级联动菜单,举个最常见的例子,就是大家在填很多资料的时候,会让你选择省,市,等资料。
如果只需要省一级的选择,django可以很好的处理这种情况,它会不遗余力的帮你把数据库中所有的省取出来,但是如果有二级市一级的选择,那么它显得有点自作多情了,还是照样全部取出来,恩,中国那么多市,先不管取出来要耗费多少计算资源,光是让用户去选就得看花眼了。而我们在别人的网站上填表单的时候常见的情况是选了省之后,它才会显示市列表,并且只显示该省的市。好了,来看看如何来实现这个功能吧。
假设有如下Model:
class Province(models.Model):
name = models.CharField(max_length = 50)
def __unicode__(self):
return self.name
class City(models.Model):
name = models.CharField(max_length = 50)
province = models.ForeignKey(Province, related_name = "cities")
def __unicode__(self):
return self.name
class School(models.Model):
name = models.CharField(max_length = 50)
city = models.ForeignKey(City, related_name = "schools")
def __unicode__(self):
return self.name
class Profile(models.Model):
user = models.OneToOneField(User, related_name = "profile")
province = models.ForeignKey(Province, verbose_name = u'所在省', related_name = "profiles", null = True, blank = True)
city = models.ForeignKey(City, verbose_name = u'所在城市', related_name = "profiles", null = True, blank = True)
school = models.ForeignKey(School, verbose_name = u'所在学校', related_name = "profiles", null = True, blank = True)
def __unicode__(self):
return self.user.username
Profile Model 用来保存用户的资料。然后我们直接使用ModelForm从Profile Model创建一个Form吧,看django真是聪明,定义好了Model,其他很多事它都可以代劳。代码如下:
from django import forms
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
exclude = ('user', )
几行代码,就完成所有表单的HTML编写和后台数据验证工作, 注意我在上面生成的表单排除掉了user Field,我们可不想让用户来替别人乱填资料。
不是说有级联选择菜单吗?在哪里呢?别急,我们不需要动Form里的任何东西,这样,如果如果你的项目已经写了很多Form类,那样改起来就容易多了,因为只需要改别的地方。
接下来,先假设我们开始没预料到要使用多级联动菜单。那么你很可能有这样一个view来处理用户编辑自己资料的功能。
@login_required
def profile_edit(request):
if request.method == 'POST':
form = ProfileForm(request.POST, request.FILES, instance = request.profile)
if form.is_valid():
new_profile = form.save()
request.user.message_set.create(message=u"你的资料已经成功修改。")
return HttpResponseRedirect(reverse('paila_profile_edit', ))
else:
form =ProfileForm(instance = request.profile)
return render_to_response('profile_edit.html',locals(), context_instance = RequestContext(request))
上面的假设,我们已经通过django signal,在User Model创建的时候新实例的时候,自动创建了一个相应的profile,并且利用middleware 将相应的profile对象绑定到了request对象上。关于这些不明白的话,待会我会在下面列些参考资料。
恩,就这样,短的可怕,所有输入错误反馈,数据验证工作都已经做了。
看上去都不错,好了,直接在上面来加上多级联动菜单的功能吧。只需要在view的顶部加上几句代码,然后像是下面这样。
@login_required
def profile_edit(request):
cascade_select_list = [('province', 'city', Province, City),('city', 'school', City, School), reverse('paila_profile_edit', )]
if request.GET:
return handle_cascade_select(request, ProfileForm, cascade_select_list)
if request.method == 'POST':
form = ProfileForm(request.POST, request.FILES, instance = request.profile)
if form.is_valid():
new_profile = form.save()
request.user.message_set.create(message=u"你的资料已经成功修改。")
return HttpResponseRedirect(reverse('paila_profile_edit', ))
else:
form =ProfileForm(instance = request.profile)
return render_to_response('profile_edit.html',locals(), context_instance = RequestContext(request))
是的,只加了3行代码,接下来说明一下顶部新添加加的代码的意思。
cascade_select_list 是一个关于表示级联菜单之间关系的一个数组。除了最后一个元素表示要将处理Ajax的请求发到哪个url外,所有前面的元素都是一个有另外四个元素组成的tuple。
在这个tuple中的四个元素分别表示:
1、要监听表单中onchange事件的下拉框的name
2、第一个参数对应的下拉框发生变化的时候,要刷新的另一个下拉框的name
3、第一个下拉框对应的Model
4、要刷新的下拉框对应的Model
cascade_select_list 最后一个参数是Ajax请求的url,通过named url 来反转,其实就是最后对应的view就是 profile_edit 。所有工作都在一个view做了,由于POST方法已经用来接收表单提交的处理,所以用GET方法来提交Ajax请求。到这里,我有必要先说明一下Ajax请求是如果发过来的,在HTML中到底多了JavaScript语句。
为了尽可能少的修改原有的Form类,但是js又必须知道要对哪个表单域进行事件监听,对哪个表单域进行过滤修改。前面我们看到,cascade_select_list 是关于这些信息很好的一个来源,而且事实上有这些信息就已经足够了。在这里,我试用了template filter 将 cascade_select_list 的数据直接进行分析,生成相应的js语句。下面是该filter的代码:
from django import template
from django.utils.safestring import mark_safe
from django.conf import settings
register = template.Library()
@register.filter
def cascade_select(value):
response_url = value.pop()
mochikit_src = """
<script src="%sMochiKit/MochiKit.js" type="text/javascript"></script>
""" % settings.MEDIA_URL
script_output = u"""
<script type="text/javascript">
function on_succeed_callback_%(event_element)s(res){
filter_element = MochiKit.DOM.getElement('id_%(filter_element)s');
filter_element.parentNode.innerHTML = res.responseText;
if (select_changed_%(filter_element)s){
filter_element = MochiKit.DOM.getElement('id_%(filter_element)s');
MochiKit.Signal.connect(filter_element, "onchange", select_changed_%(filter_element)s);
}
}
function select_changed_%(event_element)s(eventObj){
target = eventObj.target();
d = MochiKit.Async.doSimpleXMLHttpRequest('%(response_url)s',{ '%(event_element)s': target.value } );
d.addCallback(on_succeed_callback_%(event_element)s);
}
event_element = MochiKit.DOM.getElement('id_%(event_element)s');
MochiKit.Signal.connect(event_element, "onchange", select_changed_%(event_element)s);
</script>
"""
output = [mochikit_src]
for event_element, filter_element, event_model, filter_model in value:
output.append(script_output % {'event_element':event_element, 'filter_element':filter_element, 'response_url':response_url})
return mark_safe(u'\n'.join(output))
cascade_select.is_safe = True
该filter从上到下各条语句的意思大概就是:
1、取出要将Ajax请求发到那个url的cascade_select_list 中的最后那个元素。
2、设置MochiKit本身的文件路径。
3、script_output是真正工作的脚本的一个模板,里面的有些字符串会被从cascade_select_list 取出来的数据替换,那样生成的js语句就可以在DOM结构中找到对应要监听事件和进行过滤的节点了。
4、从cascade_select_list 取出数据,对js模板进行替换,生成正式的js语句。
附加说明:
其实以上的js模板和语句,可以很容的被你自己喜欢的Ajax框架替换。所以我也不多解释js语句的意思了,只是,有一点需要注意的是,如果多级联动,而不是二级联动,那么就要到由于使用innerHTML替换中间某级节点的话,那么他原本注册的事件监听就失效了,所以在上面的语句中有代码
if (select_changed_%(filter_element)s){
filter_element = MochiKit.DOM.getElement('id_%(filter_element)s');
MochiKit.Signal.connect(filter_element, "onchange", select_changed_%(filter_element)s);
}
来检测一下,过滤之后的某个下拉框是否也是要过滤其他下拉框的节点,从而再次注册监听事件。
如何在模板中使用?,恩,大概像是下面这样:
{% block content %}
<h1>修改资料</h1>
{% if form.errors %}
<p class="errors">请修改下面的错误: {{ form.non_field_errors }}</p>
{% endif %}
<form method="post" action="" enctype="multipart/form-data">
<table>
{{form}}
</table>
{%load trade_tags%}
{{cascade_select_list|cascade_select}}
<p class="submit"><input type="submit" value="修改"></p>
</form>
{% endblock %}
与原来相比只修改,只修改了一个地方,在form下面加载进新定义的 cascade_select filter的所在的Module,对 cascade_select_list 使用该filter,就可以生成需要的js语句了。
最终以上生成的js语句会在change事件发生的时候对指定的url发起GET请求,将该表单域的name和value作为参数传递给服务器。比如province改变的话会产出类似下面url进行请求:
/accounts/profile/edit/?province=1
好了,既然Ajax请求进来了,那么再回来说说view函数该如何处理。
在view函数的开头新加的语句里只有两句用来应付新增加AJax请求:
if request.GET:
return handle_cascade_select(request, ProfileForm, cascade_select_list)
这里if request.GET:是用来判断是否有GET参数传进来,即结尾的?province=1这样的查询字符串,由于整个表单本身的数据是通过POST提交的,所以光这个就可以区分开这个view要处理的3种情况:
1、直接打开url要进行profile修改时,即既没有GET也没有POST参数。
2、Ajax请求,只有GET参数
3、表单提交请求,只有POST参数
当然这里的判断是简单了点,如果你的实际情况复杂还是很容易修改的的。
好了,如果是一个Ajax请求的话,所有工作就交给一个叫做handle_cascade_select的函数来完成。这是在相对常见的情况下可以采取的处理方法。
来看它的代码:
def handle_cascade_select(request, form_class, cascade_select_list):
cascade_select_list.pop()
form =form_class()
for event_element, filter_element, event_model, filter_model in cascade_select_list:
if event_element in request.GET:
try:
event_element_object = get_object_or_404(event_model, id = int(request.GET[event_element]))
except Exception:
form.fields[filter_element].queryset = filter_model.objects.none()
else:
form.fields[filter_element].queryset = filter_model.objects.extra(where=['%s_id = %s' % (event_element, event_element_object.id)])
return HttpResponse(str(form[filter_element]))
它接受一个request对象,一个表单类,在这里我们要传的是ProfileForm,以及用来表示级联关系的cascade_select_list。
简单来说他就是再次实例化整个ProfileForm,然后根据request.GET中的参数名和参数值,即类似 ?province=1 这样的名值对。遍历 cascade_select_list 中是否有相应的需要处理的表单域name。由于在cascade_select_list 还设置了相应的事件和要过滤的Model类,那么如果cascade_select_list 中有相应的需要处理的表单域name就可以进行以下简单的处理(在这里以?province=1为例):
1、找到 Province id为 1 的数据库记录,如果找不到或产生其他任何异常,那么进行 2,否则为 3
2、将ProfileForm中相应的要过滤的那个表单域在这里是city的queryset设置为空queryset。
3、如果查到了Province id为 1的数据库记录,那么就查到City 模型province_id为刚刚找到的province的id。从而找出该省所有的城市。并且将该结果作为要过滤的那个表单域的queryset。
可以看到这里处理第三步的情况的是要符合很多条件的:
ProfileForm中表单域的name刚好和Model中对应的字段名字一致。
要过滤的Model在数据库表中对应的要查询的外键名刚好是 'name'_id的形式(当然这是的django帮你生成SQL时默认情况)。
所以,如果是更复杂的过滤条件还是请自己写点代码来处理吧。
现在要过滤的那个表单域的queryset已经被修改了,我们只需要该表单域,而不是整个表单,所以只取出该表单域,然后作为HttpResponse对象返回给浏览器。浏览器就可以收到只包含修改后queryset对应的选项的一个下拉框了。
浏览器直接将改反馈结果作为innerHTML替换原来的那个表单域即可。由于替换前后两个表单域都是通过ProfileForm来生成的,所以替换的结果,显而易见除了下拉选项不同,其他完全相同。
至此,一个多级联动菜单就完成了。其实上面还没处理一个情况就是用户第一次打开页面的时候,即:
1、直接打开url要进行profile修改时,即既没有GET也没有POST参数。
各级下拉框依旧包含所有选项,所以这种情况下最好还是对ProfileForm中那些一级以下的表单域的queryset进行一下修改。
最后修改的view大概就是这样:
@login_required
def profile_edit(request):
cascade_select_list = [('province', 'city', Province, City),('city', 'school', City, School), reverse('paila_profile_edit', )]
if request.GET:
return handle_cascade_select(request, ProfileForm, cascade_select_list)
if request.method == 'POST':
form = ProfileForm(request.POST, request.FILES, instance = request.profile)
if form.is_valid():
new_profile = form.save()
request.user.message_set.create(message=u"你的资料已经成功修改。")
return HttpResponseRedirect(reverse('paila_profile_edit', ))
else:
form =ProfileForm(instance = request.profile)
if request.profile.province:
form.fields['city'].queryset = request.profile.province.cities
else:
form.fields['city'].queryset = City.objects.none()
if request.profile.city:
form.fields['school'].queryset = request.profile.city.schools
else:
form.fields['school'].queryset = School.objects.none()
return render_to_response('profile_edit.html',locals(), context_instance = RequestContext(request))
可以看到,Django虽然没有绑定任何Ajax框架,但是借助已有的Ajax框架要实现动态的功能还是很简单的。尤其是借助自定义template filter 和tag 技术,完全可以将现有的Ajax框架封装起来,形成像ROR有的那样一个比较好用的Ajax库。
分享到:
相关推荐
主要介绍了Django自关联实现多级联动查询实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
多级菜单在很多上面都有应用,这篇文章主要介绍了Django Admin实现三级联动(省市区),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
使用Django+MySQL实现的在线电影推荐系统源码 使用Django+MySQL实现的在线电影推荐系统源码 使用Django+MySQL实现的在线电影推荐系统源码 使用Django+MySQL实现的在线电影推荐系统源码 使用Django+MySQL实现...
使用Django实现的文件分享系统,实现搜索功能,分享功能,用户分享文件查询,写该项目的目的主要是用于保存一些电子书籍和学习资料,方便自己和其他人查找资料,所以对文件大小进行限制最大为5M。 使用Django...
利用django实现了省市县的级联下拉。数据库采用的是sqlite。
使用Django和python实现一个智能教室管理系统.zip
Django实现商城网站源码 Django实现商城网站源码 Django实现商城网站源码 Django实现商城网站源码 Django实现商城网站源码 Django实现商城网站源码 Django实现商城网站源码 Django实现商城网站源码 Django...
python+Django+layui实现婚庆系统源码 python+Django+layui实现婚庆系统源码 python+Django+layui实现婚庆系统源码 python+Django+layui实现婚庆系统源码 python+Django+layui实现婚庆系统源码 python+...
python3.7下Django2.2引用ztree插件从mysql数据库导入数据表来实现树形态目录。
Scrapy爬取去哪儿网,并使用Django框架+PyEcharts实现可视化大屏。 Scrapy爬取去哪儿网,并使用Django框架+PyEcharts实现可视化大屏。 Scrapy爬取去哪儿网,并使用Django框架+PyEcharts实现可视化大屏。 Scrapy爬取...
使用Django和Python实现一个智能教室管理系统源码 使用Django和Python实现一个智能教室管理系统源码 使用Django和Python实现一个智能教室管理系统源码 使用Django和...
Django实现的图书借阅系统 Django实现的图书借阅系统 Django实现的图书借阅系统 Django实现的图书借阅系统 Django实现的图书借阅系统 Django实现的图书借阅系统 Django实现的图书借阅系统 Django实现的图书...
使用django和小程序构建的时间管理助手源码 使用django和小程序构建的时间管理助手源码 使用django和小程序构建的时间管理助手源码 使用django和小程序构建的时间管理助手源码 使用django和小程序构建的时间...
Python基于Django框架实现的一个学生信息管理系统源码 Python基于Django框架实现的一个学生信息管理系统源码 Python基于Django框架实现的一个学生信息管理系统源码 Python基于...
使用Django开发的天天生鲜商城源码 使用Django开发的天天生鲜商城源码 使用Django开发的天天生鲜商城源码 使用Django开发的天天生鲜商城源码 使用Django开发的天天生鲜商城源码 使用Django开发的天天生鲜...
使用Django框架开发的企业OA管理系统源码 使用Django框架开发的企业OA管理系统源码 使用Django框架开发的企业OA管理系统源码 使用Django框架开发的企业OA管理系统源码 使用Django框架开发的企业OA管理系统源码 ...
Django实现在线视频课堂播放网站源码 Django实现在线视频课堂播放网站源码 Django实现在线视频课堂播放网站源码 Django实现在线视频课堂播放网站源码 Django实现在线视频课堂播放网站源码 Django实现在线视频...
python+django+bootstrap实现的管理系统,注释全,功能强大,又文档说明,导入即可查看源码,初学者必备
使用Django搭建的音乐网站管理系统,具有完整的音乐搜索,在线播放,下载,评论,登陆,榜单,分类等功能。 使用Django搭建的音乐网站管理系统,具有完整的音乐搜索,在线播放,下载,评论,登陆,榜单,分类等...