关于本站
1、基于Django+Bootstrap开发
2、主要发表本人的技术原创博客
3、本站于 2015-12-01 开始建站
上一篇文章处理了博文筛选,采用QuerySet方式处理。
但排序这里用QuerySet方式无法展开手脚,因为排序涉及到的模型比较复杂。例如按照评论数排序,评论是在django-comments中的模型。而且和博文的模型没有直接关系,只是通过ContentType方式连接。另阅读数那个也是如此,阅读数我在前面也使用了ContentType,可参考:阅读统计分析一文。
最后,想不出有什么比较好的方法。决定使用原始的SQL查询语句来处理。
同样,接下来的开发我总结一套流程:先前端,再后端,最后反馈前端。
这个页面在写上一篇博文的时候已经设计好了:
上面3个下拉框都是用于排序,还有一个标签显示当前以何种方式排序。
下拉框这个组件是Bootstrap的,取其中一个的html代码如下:
<div class="filter-item"> <div class="btn-group"> <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"> 发布日期 <span class="caret"></span> </button> <ul class="dropdown-menu" role="menu"> <li><a href="javascript:void(0);"> 发布日期从近到远 </a></li> <li><a href="javascript:void(0);"> 发布日期从远到近 </a></li> </ul> </div> <!--其他下拉框也在这个filter-item的div里面--> </div>
页面设计好之后,想办法把数据提交给后端。告知后端,你想干嘛。
这里有3种排序类型,每种类型都有升序和降序两种方法。那么我们可以给后端用GET方式提交两个参数:1个参数是排序类型;另1个参数是升序还是降序。
提交数据自然可以想到Form表单。我们可以在Form表单中加入类型为hidden的input标签,再通过点击对应的选项修改其value值。
Form表单在上一篇写筛选的博文已经提到了,加入排序的input标签:
<form id="filter_form" method="GET" action="{%url 'blog_list'%}"> <!--排序类型--> <input type="hidden" name='sf' id='sort_sf' value='' /> <!--排序方式--> <input type="hidden" name='st' id='sort_st' value='' /> </form>
再给上面的3个下拉框里面选项绑定点击代码,让其点击之后给这两个input标签写对应的value值。
因为执行代码一样的,写个通用的函数提供调用,如下js/jQuery代码:
//排序可调用的通用函数 function blog_sort(sf, st){ $('#sort_sf').attr('value', sf); //排序类型 $('#sort_st').attr('value', st); //排序方式(升序/降序) $('#filter_form').submit(); //提交表单 }
再给上面对应选项绑定点击事件(为了方便看代码,其他标签就不显示,只显示对应标签):
<div class="filter-item"> <!--其他不相干的html代码不显示--> <a href="javascript:void(0);" onclick="blog_sort('date','-1');">发布日期从近到远</a> <a href="javascript:void(0);" onclick="blog_sort('date','1');">发布日期从远到近</a> <!--其他不相干的html代码不显示--> <a href="javascript:void(0);" onclick="blog_sort('view','-1');">阅读数由多到少</a> <a href="javascript:void(0);" onclick="blog_sort('view','1');">阅读数由少到多</a> <!--其他不相干的html代码不显示--> <a href="javascript:void(0);" onclick="blog_sort('comment','-1');">评论数由多到少</a> <a href="javascript:void(0);" onclick="blog_sort('comment','1');">评论数由少到多</a> </div>
此处,排序类型有:date、view和comment3种。降序是-1,升序是1。
前端代码就写到这里,接下来需要在后端处理提交的数据。
打开对应的views.py文件。上一篇博文处理筛选的方式这里就不再使用了,需要写SQL语句来处理。
这里可能有人不太了解ContentType是什么。ContentType其实很简单,为了给表关联其他任意表。
在数据库中体现为两个字段:1个char字段标记关联哪个表和1个int字段标记该表对应记录的id主键值。
你可以用可视化工具打开你的数据库。由于我的数据库暂时还是SQLite的,可以用SQLiteSpy打开。找到django-comments评论库的表django_comments:
可以看到有两个字段:content_type_id和object_pk。
这个content_type_id为9是我这个博文应用对应的类型值,该值可以在django_content_type查得。
object_pk是对应的博文id值。例如图片第一条记录表示有人在我博文id为5评论了一条“good”。
先不理会其他筛选条件,对评论数排序的SQL语句如下:
/*按照评论数排序*/ select blog_blog.* from blog_blog left join (select * from django_comments where content_type_id =9 and root_id=0) as c on blog_blog.id = c.object_pk group by blog_blog.id order by count(c.object_pk) desc
其中,and root_id=0 不用加入,因为该字段是我自己修改django-comments库加入的,为了实现回复评论功能。
通过该方法即可统计对应评论数,再进行降序排列。
若加入我博文其他筛选条件,此SQL语句可以改为:
/*按照评论数排序和in筛选*/ SELECT b.* from ( SELECT blog_blog.* FROM blog_blog left JOIN blog_blog_tags ON (blog_blog.id = blog_blog_tags.blog_id) WHERE blog_blog_tags.tag_id IN (2, 4) ) as b left join (select * from django_comments where content_type_id =9 and root_id=0) as c on b.id = c.object_pk group by b.id order by count(c.object_pk) desc
首先,blog和分类标签tag先联合查询,筛选对应的tag,并重命名为b。
再和评论表关联,按照评论数排序。
其他两种排序类型都差不多可以用这种方式,但该部分定制化比较高,只能大致讲个原理。最后总结SQL模型如下:
[blog_blog join on blog_tag where tag|recommend ] as b join on django_comments|view_num as c group by b.id order by b.publish_time|count(c.object_pk)|c.view_num
不管多少个条件,最终需要拼凑出一个SQL查询语句。由于刚好我这些可以写出一个结构很类似的SQL,只需要替换部分内容即可。
最终写出来的代码有点长,如下(部分代码在上一篇博文有讲解过,就不再赘述):
#coding:utf-8 from django.shortcuts import render_to_response from django.http import Http404 from django.template import RequestContext from django.contrib.contenttypes.models import ContentType from apps_project.blog.models import Blog,Tag #我的应用模型 def index(request): try: #处理分类标签# tags_checked = request.GET.getlist('tag') #获取同名参数 #判断是否需要勾选 tag_need_length = len(tags_checked) tag_need_check = tag_need_length>0 and tag_need_length<Tag.objects.count() tags = [] tag_ids = [] for tag in Tag.objects.all(): tag.count = len(tag.blog_set.all()) tag.checked = str(tag.id) in tags_checked and tag_need_check tags.append(tag) if tag.checked: tag_ids.append(str(tag.id)) #重新建一个list避免客户端乱输参数 #博文筛选 Blog的条件 blog_where = [] #分类标签筛选条件 if tag_need_length: blog_where.append('blog_blog_tags.tag_id IN (%s)' % ','.join(tag_ids)) #推荐博文筛选条件 is_recommend = request.GET.get('recommend','') == 'true' if is_recommend: blog_where.append('blog_blog.recommend = 1') #SQL语句处理 #前半部分(筛选部分) sql_first = '''SELECT b.* from (SELECT blog_blog.* FROM blog_blog left JOIN blog_blog_tags ON (blog_blog.id = blog_blog_tags.blog_id) WHERE %s) as b ''' % (' and '.join(blog_where) if len(blog_where) else '1=1') #中间部分(评论和阅读排序部分) blog_type_id = ContentType.objects.get_for_model(Blog).id #Blog的content_type_id content_type_where = 'content_type_id=%s' % (blog_type_id) sorted_field = request.GET.get('sf', 'date') #获取排序类型 if sorted_field == 'comment': sql_mid = '''left join (select * from %s where %s and root_id=0) as c on b.id = c.%s group by b.id order by %s ''' % ( 'django_comments', content_type_where, 'object_pk', 'count(c.object_pk)' ) elif sorted_field == 'view': sql_mid = '''left join (select * from %s where %s) as c on b.id = c.%s group by b.id order by %s ''' % ( 'view_record_viewnum', content_type_where, 'object_id', 'c.view_num' ) else: sql_mid = 'group by b.id order by %s ' % 'b.publish_time' sorted_field = 'date' #收尾部分(降序还是升序) sorted_type = request.GET.get('st', '-1') sql_last = 'desc' if sorted_type == '-1' else '' #SQL语句组合,并返回对应的模型对象 sql = sql_first + sql_mid + sql_last blogs = Blog.objects.raw(sql) add_len_to_raw_query(blogs) #加上len()方法,给分页器使用 #分页(自定义方法,使用Django自带的分页器) paginator, blogs = getPages(request, blogs) #返回参数处理 params = {} params['recommend'] = is_recommend params['check_all'] = not(tag_need_check) params['sorted_field'] = sorted_field params['sorted_type'] = sorted_type #return data data = {} data['filter'] = params #返回参数,需要设置页面 data["tags"] = tags data["blogs"]=blogs data["paginator"]=paginator except Exception: raise Http404 return render_to_response('blog/blog_filter.html',data) #给rawqueryset对象加上len()方法 def add_len_to_raw_query(query): from django.db.models.query import RawQuerySet def __len__(self): from django.db import connection sql = 'select count(*) from (%s) as newsql' % query.raw_query with connection.cursor() as cursor: cursor.execute(sql) row = cursor.fetchone() return row[0] setattr(RawQuerySet, '__len__', __len__)
鉴于代码比较多,我挑几个关键的地方说明一下:
1)执行SQL语句返回对应的模型对象,可以用该模型的raw方法。例如Blog.objects.raw(sql)。该SQL语句也是有要求,必须只能而且返回该模型的全部字段。
2)Django自带的分页器Paginator由于需要统计个数,所以会使用len()方法。若不做其他任何处理会出现如下错误:
可以在执行SQL语句之后,再加个list函数转换为list。但这种方法效率不高,最好使用SQL语句去统计记录数。如上代码,我加了一个add_len_to_raw_query方法。
弄完这些,可以测试一下效果。可以正常筛选和排序,但页面始终没有显示当前的排序方式,以及排序之后,再筛选会丢失之前的排序信息。这个就需要反馈前端了。
先解决排序之后,再筛选会丢失之前的排序信息问题。
这个是因为没有给前面表单中的hidden类型的input标签设置value值,上面后端处理代码已经将对应的排序参数记录在filter中,并返回。只需要修改一下模版文件:
<!--排序类型--> <input type="hidden" name='sf' id='sort_sf' value='{{filter.sorted_field}}' /> <!--排序方式--> <input type="hidden" name='st' id='sort_st' value='{{filter.sorted_type}}' />
这样再提交这个表单就保持原来的排序方式。
至于显示排序方式的文本,可以判断排序类型,直接从对应的排序选项中把文本拿出来。
为了方便找具体是哪个排序类型,给排序选项加上name属性:
<div class="filter-item"> <!--其他不相干的html代码不显示--> <a href="javascript:void(0);" onclick="blog_sort('date','-1');" name="date">发布日期从近到远</a> <a href="javascript:void(0);" onclick="blog_sort('date','1');" name="date">发布日期从远到近</a> <!--其他不相干的html代码不显示--> <a href="javascript:void(0);" onclick="blog_sort('view','-1');" name="view">阅读数由多到少</a> <a href="javascript:void(0);" onclick="blog_sort('view','1');" name="view">阅读数由少到多</a> <!--其他不相干的html代码不显示--> <a href="javascript:void(0);" onclick="blog_sort('comment','-1');" name="comment">评论数由多到少</a> <a href="javascript:void(0);" onclick="blog_sort('comment','1');" name="comment">评论数由少到多</a> </div>
而且确保每个排序类型中,第1个是降序,第2个是升序。在页面加载执行代码部分写js/jQuery代码:
/*设置排序文本*/ function set_sort_content(field, sort_type){ var texts = $('.filter-item a[name=' + field + ']'); var text = ''; if(texts.length>1){ text = texts[sort_type=='-1' ? 0:1].text.trim(); } $('#sort_content').text(text); } //执行设置排序文本 set_sort_content('{{filter.sorted_field}}', '{{filter.sorted_type}}');
通过返回前端的数据,执行设置排序文本的函数即可。
呼~这篇博文有点长,不知道看懂的人有多少。至少中间那个长长的代码有耐心看的人很少,而且不怎么通用。至少其他部分个人感觉讲得比较清晰。还是得把录制Django开发的教学视频提上日程。