Django 3.0实战: 仿链家二手房信息查询网(附GitHub源码)

编程技术圈

共 9170字,需浏览 19分钟

 · 2020-07-28

今天小编我要带你使用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 myhouseprojectcd myhouseprojectdjango-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.name
class 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.A1 elif 50 <= self.area < 70: self.area_class = Area.A2 elif 70 <= self.area < 90: self.area_class = Area.A3 elif 90 <= self.area < 140: self.area_class = Area.A4 else:            self.area_class = Area.A5        super().save(*args, **kwargs) 

 现在进入myhouseproject文件夹,输入如下命令创建House模型对应数据表和超级用户了。


python manage.py makemigrationspython manage.py migratepython 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, Communityclass 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 pathfrom . import views
# namespaceapp_name = 'house'urlpatterns = [    # 展示文章列表并筛选  path('', views.house_filter, name='house_filter'), ]

我们还要将这个app的urls加入到项目urls中去,如下所示:

#myhouseproject/urls.py

from django.contrib import adminfrom django.urls import path, include
urlpatterns = [ 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, Directionimport django_filtersfrom django.db.models import Q# Filter house by city, bedroom number, floor and areaclass 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 = House        fields = { }

编写视图函数

我们的house_filter视图函数也很简单,如下所示。

#house/views.py

from django.shortcuts import renderfrom .models import Housefrom .filters import HouseFilterfrom django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
# Create your views here.# Filter housesdef 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 False context = {'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"> <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="http://www.w3.org/2000/svg"> <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 %} <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 request.GET.city %} 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 %} <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 %} <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 %} <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="http://www.w3.org/2000/svg"> <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__.pycore_tags_filters.py,如下所示:

core_tags_filters.py添加如下代码,即可在模板中使用{% load core_tags_filter %}调用 {% param_replace %}这个自定义模板标签了。

from django import templateregister = 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] = v for 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/就可以看到文初熟悉的画面了。是不是很简单而功能强大?

源码地址

https://github.com/shiyunbo/django-house-filter


浏览 26
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报