
很久没写原创文章了,真对不住大家。今天小编我要带你使用Django开发一个APP,仿链家的二手房信息查询。它只有一个页面,主要功能用于展示二手房信息,并支持访问用户根据关键词或多个筛选条件查询房源信息。是的,你没听错。我们只开发一个页面,只编写一个视图函数,只使用一个模板,然而其实现的筛选查询功能确实非常有用的,可以扩展到其它项目。文末会附上GITHUB源码地址。
我们要实现的最终展示效果如下图所示:

所用到的安装包
Django==3.0.8 # 最新Django版本
django-filter==2.3.0 # 扩展Django的Filter功能
前端使用bootstrap 4。如果你不熟悉django-filter的使用,强烈建议先阅读Django-filter教程详解: 从安装使用到高阶美化分页-大江狗精品
项目开始
先使用pip安装项目所使用到的安装包,然后使用如下命名创建项目(project)和应用(app)。项目名为myhouseproject, app名为house。
django-admin startporject myhouseproject
cd myhouseproject
django-admin startapp house
然后在项目 settings.py 文件的 INSTALLED_APPS 中添加应用名称:
INSTALLED_APPS = ['django.contrib.admin','django.contrib.auth','django.contrib.contenttypes','django.contrib.sessions','django.contrib.messages','django.contrib.staticfiles','house',
]
编写模型
进入house文件夹,编写models.py,添加如下代码。Django的ORM会自动根据模型在自带的sqlite数据库中生成数据表。我们的House模型与Community模型是一对多的关系(ForeignKey),因为一个Community(小区)内包含多条House信息。
#house/models.py
from django.db import models
# Create your models here.
class City(models.TextChoices):BEIJING = 'bj', '北京'SHANGHAI = 'sh', '上海'SHENZHEN = 'sz', '深圳'GUANGZHOU = 'gz', '广州'HANGZHOU = 'hz', '杭州'
class Bedroom(models.TextChoices):B1 = '1', '1室1厅'B2 = '2', '2室1厅'B3 = '3', '3室1厅'B4 = '4', '4室2厅'
class Area(models.TextChoices):A1 = '1', '<50平米'A2 = '2', '50-70平米'A3 = '3', '70-90平米'A4 = '4', '90-140平米'A5 = '5', '>140平米'
class Floor(models.TextChoices):LOW = 'l', '低楼层'MIDDLE = 'm', '中楼层'HIGH = 'h', '高楼层'
class Direction(models.TextChoices):EAST = 'e', '东'SOUTH = 's', '南'WEST = 'w', '西'NORTH = 'n', '北'
class Community(models.Model):name = models.CharField(max_length=60, verbose_name='小区')city = models.CharField(max_length=2, choices=City.choices, verbose_name="城市")add_date = models.DateTimeField(auto_now_add=True, verbose_name="发布日期")mod_date = models.DateTimeField(auto_now=True, verbose_name="修改日期")class Meta:verbose_name = "小区"verbose_name_plural = "小区"def __str__(self):return self.nameclass House(models.Model):description = models.CharField(max_length=108, verbose_name="描述")community = models.ForeignKey('Community', on_delete=models.CASCADE, verbose_name="小区")bedroom = models.CharField(max_length=1, choices=Bedroom.choices, verbose_name="房型")direction = models.CharField(max_length=2, choices=Direction.choices, verbose_name="朝向")floor = models.CharField(max_length=1, choices=Floor.choices, verbose_name="楼层")area = models.DecimalField(max_digits=8, decimal_places=2, verbose_name="面积(平方米)")area_class = models.CharField(max_length=1, null=True, blank=True, choices=Area.choices, verbose_name="面积")price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name="价格(万元)")add_date = models.DateTimeField(auto_now_add=True, verbose_name="发布日期")mod_date = models.DateTimeField(auto_now=True, verbose_name="修改日期")class Meta:verbose_name = "二手房"verbose_name_plural = "二手房"def __str__(self):return '{}.{}'.format(self.description, self.community)def save(self, *args, **kwargs):if self.area < 50:self.area_class = Area.A1elif 50 <= self.area < 70:self.area_class = Area.A2elif 70 <= self.area < 90:self.area_class = Area.A3elif 90 <= self.area < 140:self.area_class = Area.A4else:self.area_class = Area.A5super().save(*args, **kwargs)
现在进入myhouseproject文件夹,输入如下命令创建House模型对应数据表和超级用户了。
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
之所以我们要创建超级用户admin是因为我们要通过Django自带的后台admin添加房产信息。Django的后台admin虽然不太美观,但功能强大,使我们可以专注于向用户展示信息。
自定义Admin并添加数据
进入house文件夹,编写admin.py,添加如下代码,将House和Community模型在后台注册。
#house/admin.py
from django.contrib import admin
# Register your models here.
from .models import House, Community
class CommunityAdmin(admin.ModelAdmin):'''设置列表可显示的字段'''list_display = ('name', 'city', )'''每页显示条目数'''list_per_page = 10'''设置可编辑字段'''list_editable = ('city',)'''按发布日期排序'''ordering = ('-mod_date',)class HouseAdmin(admin.ModelAdmin):'''表单字段'''fields = ('description', 'community', 'bedroom', 'direction', 'floor', 'area', 'price', )'''设置列表可显示的字段'''list_display = ('description', 'community', 'price', 'bedroom', 'direction', 'floor', 'area', 'area_class', )'''设置过滤选项'''list_filter = ('bedroom', 'direction', 'floor', 'area_class')'''每页显示条目数'''list_per_page = 10'''设置可编辑字段'''list_editable = ('bedroom', 'direction', 'floor', 'area_class',)'''raw_id_fields'''raw_id_fields = ('community',)'''按发布日期排序'''ordering = ('-mod_date',)admin.site.register(Community, CommunityAdmin)
admin.site.register(House, HouseAdmin)
模型注册好后,使用python manage.py runserver即可启动测试服务器。此时访问http://127.0.0.1:8000/admin/可进入后台添加数据,如下图示:

向用户展示数据
我们现在要编写向用户展示数据的url, 并将其指向house_filter的视图函数。
#house/urls.py
from django.urls import path
from . import views# namespace
app_name = 'house'
urlpatterns = [# 展示文章列表并筛选 path('', views.house_filter, name='house_filter'),]
我们还要将这个app的urls加入到项目urls中去,如下所示:
#myhouseproject/urls.py
from django.contrib import admin
from django.urls import path, includeurlpatterns = [path('admin/', admin.site.urls),path('', include('house.urls')),
]
现在我们可以专心写我们的视图函数house_filter了,不过我们希望视图函数中使用Django-filter,所以还需自定义以何种条件筛选查询数据集的filter。
自定义Filter
进入house文件夹,新建filters.py, 添加如下代码。我们定义的HouseFilter类包括关键词、城市、房型、楼层和面积等等。
#house/filters.py
from .models import House, City, Bedroom, Floor, Area, Direction
import django_filters
from django.db.models import Q# Filter house by city, bedroom number, floor and area
class HouseFilter(django_filters.FilterSet):'''根据城市,房型,面积,楼层和朝向筛选二手房 '''q = django_filters.CharFilter(method='my_custom_filter')city = django_filters.ChoiceFilter(field_name='community__city', choices=City.choices,label='城市')bedroom = django_filters.ChoiceFilter(field_name='bedroom', choices=Bedroom.choices,label='房型')area = django_filters.ChoiceFilter(field_name='area_class', choices=Area.choices,label='面积')floor = django_filters.ChoiceFilter(field_name='floor', choices=Floor.choices,label='楼层')direction = django_filters.ChoiceFilter(field_name='direction', choices=Direction.choices,label='楼层')def my_custom_filter(self, queryset, q, value):return queryset.filter(Q(description__icontains=value) | Q(community__name__icontains=value))class Meta:model = Housefields = { }
编写视图函数
我们的house_filter视图函数也很简单,如下所示。
#house/views.py
from django.shortcuts import render
from .models import House
from .filters import HouseFilter
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger# Create your views here.
# Filter houses
def house_filter(request):base_qs = House.objects.all().select_related('community')f = HouseFilter(request.GET, queryset=base_qs)paginator = Paginator(f.qs, 5)page = request.GET.get('page', 1)try:page_obj = paginator.page(page)except PageNotAnInteger:page_obj = paginator.page(1)except EmptyPage:page_obj = paginator.page(paginator.num_pages)is_paginated = True if paginator.num_pages > 1 else Falsecontext = {'page_obj': page_obj, 'paginator': paginator, 'is_paginated': is_paginated, 'filter': f, }return render(request, 'house/house_filter.html', context)
编写模板
我们的模板继承了templates/house/base.html, 主要bootstrap 4及自定义的样式,这里就不贴出了,大家可以在源码下载。house_filter.html核心代码如下所示, 最上面部分是一个搜索框,中间部分是过滤选项,下面表格用于展示结果,最下面是分页。
#templates/house/house_filter.html
{% extends 'house/base.html' %}
{% load static %}
{% load core_tags_filters %}{% block content %}
<div class="py-4 px-3 bg-light"><div class="container"><!-- Nav tabs 3 --><div class="row"><div class="col-1 col-md-2"></div><div class="col-10 col-md-8"><ul class="nav nav-tabs px-1 mx-0" id="myTab" role="tablist"><li class="nav-item"><a class="nav-link active" id="home-tab" data-toggle="tab" href="#tabpanel1" role="tab" aria-controls="home" aria-selected="true">二手房</a></li><li class="nav-item"><a class="nav-link" id="profile-tab" data-toggle="tab" href="#tabpanel2" role="tab" aria-controls="profile" aria-selected="false">新房</a></li><li class="nav-item"><a class="nav-link" id="messages-tab" data-toggle="tab" href="#tabpanel3" role="tab" aria-controls="messages" aria-selected="false">租房</a></li></ul></div><div class="col-1 col-md-8"></div></div><div class="tab-content"><div class="tab-pane fade show active" id="tabpanel1" role="tabpanel" aria-labelledby="home-tab"><div class="row pt-3"><div class="col-1 col-md-2"></div><div class="col-10 col-md-8"><form role="form" method="get" action="{% url 'house:house_filter' %}"><div class="input-group"><input type="text" name="q" value="{% if filter.form.q.value %}{{ filter.form.q.value }}{% endif %}" class="form-control" id="id_q" placeholder="关键词或小区名"><div class="input-group-append"><button type="submit" class="btn btn-inline btn-sm bg-warning"><svg class="bi bi-search" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="SVG namespace"><path fill-rule="evenodd" d="M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z"/><path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/></svg></button></div></div></form></div><div class="col-1 col-md-2"></div></div></div><div class="tab-pane fade" id="tabpanel2" role="tabpanel" aria-labelledby="profile-tab"></div><div class="tab-pane fade" id="tabpanel3" role="tabpanel" aria-labelledby="messages-tab"></div></div></div>
</div><div class="py-4 px-3 bg-white"><div class="container"><table class="mb-4" style="font-size:14px"><tbody>{% with field=filter.form.city %}<!-- checkbox --><tr class="mt-2"><td class="align-text-top" style="width:40px;"><span><b>城市</b></span></td><td class="tb-filter-item"><a href="?{% param_replace city='' %}"><input type="checkbox"{% if not Этот домен уже направлен на наши NS-сервера. Осталось добавить его в личном кабинете и он заработает. %}checked="checked"{% endif %}disabled /><span>全部</span></a>{% for pk, choice in field.field.widget.choices %}{% ifnotequal forloop.counter0 0 %}<a href="?{% param_replace city=pk %}" class="align-items-center"><input id="id_{{field.name}}_{{ forloop.counter0 }}" name="{{field.name}}"type="checkbox" value="{{pk}}" class=""{% ifequal field.value pk %}checked="checked"{% endifequal %} disabled /><span>{{ choice }}</span></a>{% endifnotequal %}{% endfor %}</td></tr>{% endwith %}{% with field=filter.form.bedroom %}<!-- checkbox --><tr><td class="align-text-top" style="width:40px;"><span><b>{{ field.label }}</b></span></td><td class="tb-filter-item"><a href="?{% param_replace bedroom='' %}"><input type="checkbox"{% if not request.GET.bedroom %}checked="checked"{% endif %}disabled /><span>全部</span></a>{% for pk, choice in field.field.widget.choices %}{% ifnotequal forloop.counter0 0 %}<a href="?{% param_replace bedroom=pk %}" class="align-items-center"><input id="id_{{field.name}}_{{ forloop.counter0 }}" name="{{field.name}}"type="checkbox" value="{{pk}}" class=""{% ifequal field.value pk %}checked="checked"{% endifequal %} disabled /><span>{{ choice }}</span></a>{% endifnotequal %}{% endfor %}</td></tr>{% endwith %}{% with field=filter.form.area %}<!-- checkbox --><tr class="mt-2"><td class="align-text-top" style="width:40px;"><span><b>{{ field.label }}</b></span></td><td class="tb-filter-item"><a href="?{% param_replace area='' %}"><input type="checkbox"{% if not request.GET.area %}checked="checked"{% endif %}disabled /><span>全部</span></a>{% for pk, choice in field.field.widget.choices %}{% ifnotequal forloop.counter0 0 %}<a href="?{% param_replace area=pk %}" class="align-items-center"><input id="id_{{field.name}}_{{ forloop.counter0 }}" name="{{field.name}}"type="checkbox" value="{{pk}}" class=""{% ifequal field.value pk %}checked="checked"{% endifequal %} disabled /><span>{{ choice }}</span></a>{% endifnotequal %}{% endfor %}</td></tr>{% endwith %}{% with field=filter.form.direction %}<!-- checkbox --><tr class="mt-2"><td class="align-text-top" style="width:40px;"><span><b>{{ field.label }}</b></span></td><td class="tb-filter-item"><a href="?{% param_replace direction='' %}"><input type="checkbox"{% if not request.GET.direction %}checked="checked"{% endif %}disabled /><span>全部</span></a>{% for pk, choice in field.field.widget.choices %}{% ifnotequal forloop.counter0 0 %}<a href="?{% param_replace direction=pk %}" class="align-items-center"><input id="id_{{field.name}}_{{ forloop.counter0 }}" name="{{field.name}}"type="checkbox" value="{{pk}}" class=""{% ifequal field.value pk %}checked="checked"{% endifequal %} disabled /><span>{{ choice }}</span></a>{% endifnotequal %}{% endfor %}</td></tr>{% endwith %}</tbody></table><div class="x_title align-items-center py-1 row bg-white"><div class="col-6 pt-2"><h6 class="align-items-center">共找到{{ filter.qs.count }}间好房<h6></div><div class="col-6"><span class="dropdown float-right"><a href="{{ request.path }}"><button type="button" class="btn btn-sm btn-light py-1 my-0" aria-expanded="false"><svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-backspace-reverse" fill="currentColor" xmlns="SVG namespace"><path fill-rule="evenodd" d="M9.08 2H2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h7.08a1 1 0 0 0 .76-.35L14.682 8 9.839 2.35A1 1 0 0 0 9.08 2zM2 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h7.08a2 2 0 0 0 1.519-.698l4.843-5.651a1 1 0 0 0 0-1.302L10.6 1.7A2 2 0 0 0 9.08 1H2z"/><path fill-rule="evenodd" d="M9.854 5.146a.5.5 0 0 1 0 .708l-5 5a.5.5 0 0 1-.708-.708l5-5a.5.5 0 0 1 .708 0z"/><path fill-rule="evenodd" d="M4.146 5.146a.5.5 0 0 0 0 .708l5 5a.5.5 0 0 0 .708-.708l-5-5a.5.5 0 0 0-.708 0z"/></svg>清除</button></a></span></div></div><div class="table-responsive"><table class="table table-striped table-hover"><thead><tr><th scope="col">描述</th><th scope="col">小区</th><th scope="col">城市</th><th scope="col">房型</th><th scope="col">朝向</th><th scope="col">面积</th><th scope="col">价格(万元)</th></tr></thead><tbody>{% if page_obj %}{% for item in page_obj %}<tr><td>{{ item.description }}</td><td>{{ item.community }}</td><td>{{ item.community.get_city_display }}</td><td>{{ item.get_bedroom_display }}</td><td>{{ item.get_direction_display }}</td><td>{{ item.area }}</td><td>{{ item.price }}</td></tr>{% endfor %}{% endif %}</tbody></table>{% if is_paginated %}<ul class="pagination">{% if page_obj.has_previous %}<li class="page-item"><a class="page-link" href="?{% param_replace page=page_obj.previous_page_number %}">«</a></li>{% else %}<li class="page-item disabled"><span class="page-link">«</span></li>{% endif %}{% for i in paginator.page_range %}{% if page_obj.number == i %}<li class="page-item active"><span class="page-link"> {{ i }} <span class="sr-only">(current)</span></span></li>{% else %}<li class="page-item"><a class="page-link" href="?{% param_replace page=i %}">{{ i }}</a></li>{% endif %}{% endfor %}{% if page_obj.has_next %}<li class="page-item"><a class="page-link" href="?{% param_replace page=page_obj.next_page_number %}">»</a></li>{% else %}<li class="page-item disabled"><span class="page-link">»</span></li>{% endif %}</ul>{% endif %}</div></div>
</div>
{% endblock %}
注意:我们模板中还使用到了param_replace这个自定义的模板标签,用于拼接各个URL查询参数并去重,下面是详细步骤。
自定义param_replace模板标签
在house文件夹下新建templatetags文件夹,新建__init__.py和core_tags_filters.py,如下所示:

在core_tags_filters.py添加如下代码,即可在模板中使用{% load core_tags_filter %}调用 {% param_replace %}这个自定义模板标签了。
from django import template
register = template.Library()# used in django-filter preserve request paramters
@register.simple_tag(takes_context=True)
def param_replace(context, **kwargs):"""用于URL拼接参数并去重 https://stackoverflow.com/questions/22734695/next-and-before-links-for-a-django-paginated-query/22735278#22735278"""d = context['request'].GET.copy()for k, v in kwargs.items():d[k] = vfor k in [k for k, v in d.items() if not v]:del d[k]return d.urlencode()
大功告成
现在你使用python manage.py runserver启动服务器然后访问http://127.0.0.1:8000/就可以看到文初熟悉的画面了。是不是很简单而功能强大?
源码地址
shiyunbo/django-house-filter
相关阅读
Django-filter教程详解: 从安装使用到高阶美化分页-大江狗精品Django实战:Django 3.0 +Redis 3.4 +Celery 4.4异步生成静态HTML文件(附源码)
Django实战: 利用自定义模板标签实现仿CSDN博客月度归档
Django基础(16): 模板标签(tags)的分类及如何自定义模板标签
如果不想错过我们最新文章,欢迎关注【Python Web与Django开发】并加星标哦!
大江狗
2020.7