我的网站搭建(第34天) 页面改版:博文排序

  • 发布时间:2016年11月10日 11:27
  • 作者:杨仕航
* 该文是基于Python2.7开发的,最新Python3.x和Django2.x视频教程可以前往 >> Django2.0视频教程

上一篇文章处理了博文筛选,采用QuerySet方式处理。

但排序这里用QuerySet方式无法展开手脚,因为排序涉及到的模型比较复杂。例如按照评论数排序,评论是在django-comments中的模型。而且和博文的模型没有直接关系,只是通过ContentType方式连接。另阅读数那个也是如此,阅读数我在前面也使用了ContentType,可参考:阅读统计分析一文。

最后,想不出有什么比较好的方法。决定使用原始的SQL查询语句来处理。

同样,接下来的开发我总结一套流程:先前端,再后端,最后反馈前端

1、先前端:设计页面和提交数据

这个页面在写上一篇博文的时候已经设计好了:

上面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。

前端代码就写到这里,接下来需要在后端处理提交的数据。


2、再后端:处理提交的数据

打开对应的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方法。

弄完这些,可以测试一下效果。可以正常筛选和排序,但页面始终没有显示当前的排序方式,以及排序之后,再筛选会丢失之前的排序信息。这个就需要反馈前端了。


3、反馈前端:返回数据给前端

先解决排序之后,再筛选会丢失之前的排序信息问题。

这个是因为没有给前面表单中的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开发的教学视频提上日程。

上一篇:prettify.js代码高亮显示行号问题

下一篇:我的网站搭建(第33天) 页面改版:博文筛选

评论列表

智慧如你,不想发表一下意见吗?

新的评论

清空