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