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

很久没写原创文章了,真对不住大家。今天小编我要带你使用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.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 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, 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, 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 = House fields = { }

编写视图函数

我们的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 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”> <!– 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 %}”>&laquo;</a></li> {% else %} <li class=”page-item disabled”><span class=”page-link”>&laquo;</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 %}”>&raquo;</a></li> {% else %} <li class=”page-item disabled”><span class=”page-link”>&raquo;</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] = 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/就可以看到文初熟悉的画面了。是不是很简单而功能强大?

源码地址

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

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部