Job Search Engine


787 浏览 5 years, 8 months

2.8 列表过滤和搜索

版权声明: 转载请注明出处 http://www.codingsoho.com/

列表过滤和搜索

当前列表是完整显示的,记录少时看不出弊端,当记录多起来时,问题就出来了。我们来本节首先解决过滤和搜索的问题。

基本过滤

过滤有两种,GET和POST,本文搜索主要用到GET方法

原始filter方法过滤

假如我们要过滤包含”张江“地区的职位, 我们可以用http://127.0.0.1:8000/jobentry/?region=张江"访问,q后面带的就是我们的搜索关键字。我们在get_context_data函数里处理这个信息,然后将过滤结果返回给模板

class JobEntryListView(ListView):
    model = JobEntry
   def get_context_data(self, *args, **kwargs):
        context = super(JobEntryListView, self).get_context_data(*args, **kwargs)
        q = self.request.GET.get('region')
        if q:
            object_list = self.model.objects.filter(region__icontains=region)
            context['object_list'] = object_list
        return context

模板里也要加一下下面的判断,避免内容为空

{% if object_list.count %}
{% endif %}

访问上述地址,只有张江地区的岗位显示出来了。

我们也可以添加多个条件,比如除了过滤region还过滤title,可以用http://127.0.0.1:8000/jobentry/?region=张江"

代码修改为

class JobEntryListView(ListView):
    model = JobEntry
   def get_context_data(self, *args, **kwargs):
        context = super(JobEntryListView, self).get_context_data(*args, **kwargs)
        region = self.request.GET.get('region')
        title = self.request.GET.get('title')
        if region or title:
            object_list = self.model.objects.filter(region__icontains=region, title__icontains=title)
            context['object_list'] = object_list

上面代码并不严谨,只是为了说明这个逻辑,可以看到两个简单的逻辑代码已经挺复杂了。Django提供了强大的Q功能,我们可以用这个方法来改善代码。

Q搜索

接下来,我们用django的Q功能来进行逻辑计算,上面代码可以改为

        from django.db.models.query import Q
        region = self.request.GET.get('region')
        title = self.request.GET.get('title')
        if region or title:
            object_list = self.model.objects.filter(
                Q(region__icontains=region) & 
                Q(title__icontains=title))

并且它可以处理更高级的逻辑。比如,如果要求除了上面的过滤,我们希望如果只要title, description字段包含”通信“字样,那么也筛选出来,可以修改代码如下

        from django.db.models.query import Q
        q = self.request.GET.get('q')
        region = self.request.GET.get('region', None)
        title = self.request.GET.get('title', None)
        qs1 = None
        qs2 = None
        if region or title:
            query = None
            if region and title:
                query = Q(region__icontains=region) & Q(title__icontains=title)
            elif region:
                query = Q(region__icontains=region)
            else:
                query = Q(title__icontains=title)
            qs1 = self.model.objects.filter(query)
        if q:
            qs2 = self.model.objects.filter(
                Q(description__icontains=q) & 
                Q(title__icontains=q))
        if qs1 and qs2:
            object_list = (qs1 | qs2).distinct()
        elif qs1:
            object_list = qs1
        elif qs2:
            object_list = qs2
        if qs1 or qs2:
            context['object_list'] = object_list

逻辑一多代码就会复杂,Django提供了类FilterSet可以帮助我们轻松实现过滤功能。

djagno-filter

pip install django-filter

我之前用的版本是0.15.3,跟django 1.11不兼容,所以我升级到了1.0.2,但是看了一下跟最近的guide还是有很多地方不一致,所以下面的方案只针对这两个版本使用

定义过滤类

from django_filters import FilterSet, CharFilter  
class JobEntryFilter(FilterSet):
    title = CharFilter(name='title', lookup_expr='icontains', distinct=True)
    region = CharFilter(name='region', lookup_expr='icontains', distinct=True)
    class Meta:
        model = JobEntry
        fields = [
            'title',
            'region',            
        ]

在ListView添加这个filter_class

class JobEntryListView(ListView):
    model = JobEntry
    filter_class = JobEntryFilter

修改上面的get_context_data过滤代码如下

   def get_context_data(self, *args, **kwargs):
        context = super(JobEntryListView, self).get_context_data(*args, **kwargs)
        from django.db.models.query import Q
        q = self.request.GET.get('q')
        qs1 = self.filter_class(self.request.GET, self.get_queryset()).qs
        qs2 = None
        if q:
            qs2 = self.model.objects.filter(
                Q(description__icontains=q) & 
                Q(title__icontains=q)).distinct()
        if qs1 and qs2:
            object_list = (qs1 | qs2).distinct()
        elif qs1:
            object_list = qs1
        elif qs2:
            object_list = qs2
        if qs1 or qs2:
            context['object_list'] = object_list

这儿对公共搜索q还是放在了列表视图类来实现,这个其实也可以在过滤类里实现,之前版本可以用字段MethodFilter,在新版本里被method方法取代

代码如下

class JobEntryFilter(FilterSet):
    q = CharFilter(method="q_filter", distinct=True)
    title = CharFilter(name='title', lookup_expr='icontains', distinct=True)
    region = CharFilter(name='region', lookup_expr='icontains', distinct=True)
    class Meta:
        model = JobEntry
        fields = [
            'title',
            'region',            
        ]
    def q_filter(self, queryset, field, value):  
        qs = self.Meta.model.objects.filter(Q(title__icontains=value) | Q(description__icontains=value)).distinct()
        return (queryset | qs).distinct()

在列表函数里直接用一条语句来获取queryset就可以了

context['object_list'] = self.filter_class(self.request.GET, self.get_queryset()).qs

搜索表单

到目前为止,我们还都是在地址栏输入query信息,接下来我们用一个表单来传递这些值

首先安装库django-crispy-forms,这个是个强大的表单插件,不在本文详述,有兴趣可以专门看看

django-crispy-forms==1.6.0

别忘了在设置里添加

#Crispy FORM TAGs SETTINGS
CRISPY_TEMPLATE_PACK = 'bootstrap3'

使用非常简单

{% load crispy_forms_tags %}
{{ filter_form|crispy }}

模板代码如下

{% load crispy_forms_tags %}
  <div class='row'>
    <div class='col-xs-4 col-sm-3 col-md-2 left-bar'>
        <form method="GET" action="{% url 'job_entry_list' %}">
            {{ filter_form|crispy }}
            <!-- <input type='hidden' name='q' value='{{ request.GET.q }}' /> -->
            <input type='submit' class='btn btn-primary' style="margin-bottom: 5px;" value="{% trans 'Apply Filter' %}">
            <a class="btn btn-default"  style="margin-bottom: 5px;" href="{% url 'job_entry_list' %}"  >{% trans 'Clear Filters' %}</a>
        </form>
    </div>
  </div>

渲染的表单如下

作为搜索表单,我们希望排列是横着的,所以还是打算从头写一个,代码如下

.form-group input{
  text-indent: 5px;
  border-radius: 2px;
}

  <div class="row">
    <div class="col-sm-12 col-xs-0">
      <div class="collapse navbar-collapse">
        <div class="form-filter-inline">
            <form class="form-inline" method="GET" action="{% url 'job_entry_list' %}">
                {% for field in filter_form %}
                <div class="form-group">
                    {{field.label_tag}}
                    {{ field }}
                </div>                           
                {% endfor %}
                <div class="row form-group apply-filter">
                    <div class="col-sm-6">
                      <input type='submit' class='btn btn-primary' value="{% trans 'Apply Filter' %}">
                    </div>
                    <a class="btn btn-default" href="{% url 'job_entry_list' %}"  >{% trans 'Clear Filters' %}</a>
                </div>   
            </form>       
        </div>
      </div>
    </div>
  </div>

它的效果如下

添加搜索到导航条

最后,我们打算利用导航条的搜索工具栏把这个功能集成到导航栏上

修改导航条代码如下

      <form class="navbar-form navbar-left" method="get" action="{% url 'job_entry_list' %}">
        <div class="form-group">
          <input type="text" name='q' value='{{ request.GET.q }}' class="form-control" placeholder="Search">
        </div>
        <button type="submit" class="btn btn-default">Submit</button>
      </form>

主要改动如下

  1. 给form添加了method和action
  2. input输入框添加了name为q用于传递变量给后台, 并设置了value,这样能够回显地址栏的值

这样,兼顾自由搜索和固定字段搜索的功能完成了。