目录
4.1 基于路由的页面切换
4.2 开发“产品中心”模块
4.2.1 制作产品列表页面
4.2.2 分页显示
4.2.3 制作产品详情页面
4.3 富文本的概念
4.3.1 创建基于富文本的新闻模型
4.3.2 开发新闻列表和新闻详情页面
4.1 基于路由的页面切换
前面的课程已经接触过子页面的制作和使用。在制作过程中将“公司简介”模块的两个页面“企业概况”和“荣誉资质”分别进行编写,采用两个路由以及两个视图处理函数分别对请求进行处理。这种设计方式使得两个子页面完全独立,没有有效的利用其共性部分。在实际情况中,同一模块的子页面具有高度的相似性,例如本课即将制作的三个产品列表子页面,每个子页面均是相同的产品展示列表,其设计基本相同,唯一不同的是每个子页面展示不同种类的产品而已。如果还是采用页面完全独立的制作方式,当需要对某一个子页面的设计进行调整或者对某一个子页面的处理函数进行修改就需要同时修改其它子页面对应的内容,而这些修改大部分都是重复的,这种方式降低了开发效率。为了解决这个问题,一种有效的方法就是让每个子页面共享同一个访问路由并且共享同一个视图处理函数,仅需在路由后面附带不同的参数,视图处理函数根据这个附带参数来区别子页面。
为了显示链接切换效果,在具体开发前首先修改主页模块。由于后续需要对基础模板base.html进行路由修改,而主页模块目前并没有继承base.html,因此需要对主页的home.html进行修改。按照前面课程的制作方式,打开homeApp应用下的templates文件夹,编辑home.html文件,代码如下
{% extends "base.html" %}
{% load static %}
{% block title %}
首页
{% endblock %}
{% block content %}
<!-- 广告横幅 -->
<!-- 主体内容 -->
{% endblock %}
同样的,需要对homeApp中的views.py文件进行修改,重新修改home函数:
def home(request):
return render(request, 'home.html',{'active_menu': 'home',})
修改完成后保存即可。详细的主页设计将在后面的课程中介绍,这里主要是为了方便对基础模板base.html进行统一管理。
接下来正式进入“产品中心”模块的制作。在productsApp应用中创建一个templates文件夹,在该文件夹下新建一个productList.html文件作为产品列表页面。按照模板继承方式继承base.html文件,具体代码如下:
{% extends "base.html" %}
{% load static %}
{% block title %}
{{productName}}
{% endblock %}
{% block content %}
<!-- 广告横幅 -->
<div class="container-fluid">
<div class="row">
<img class="img-responsive model-img"
src="{% static 'img/products.jpg' %}">
</div>
</div>
<!-- 主体内容 -->
<div class="container">
<div class="row row-3">
<!-- 侧边导航栏 -->
<div class="col-md-3">
<div class="model-title">
产品中心
</div>
<div class="model-list">
<ul class="list-group">
<li class="list-group-item" id="robot">
<a href="{% url 'productsApp:products' 'robot' %}">家用机器人</a>
</li>
<li class="list-group-item" id="monitor">
<a href="{% url 'productsApp:products' 'monitor' %}">智能监控</a>
</li>
<li class="list-group-item" id="face">
<a href="{% url 'productsApp:products' 'face' %}">人脸识别解决方案</a>
</li>
</ul>
</div>
</div>
<!-- 说明文字和图片 -->
<div class="col-md-9">
<div class="model-details-title">
{{productName}}
</div>
<!-- 此处填入产品列表内容 -->
<div class="model-details">
</div>
</div>
</div>
</div>
{% endblock %}
上述代码有两点需要注意:
- {% block title %}处采用模板变量{{productName}}填入内容,后台在渲染过程中需要额外的传入产品类型productName参数;
- 每个产品的链接href属性均采用下述形式:
href="{% url '应用名:路由名' 字符串 %}"
上述路由通过模板标签{% url?'应用名:路由别名'?%}来逆向寻找访问网址,在该标签后面紧跟的字符串表示该路由带字符型参数,该参数将由后台进行解析。由于修改了三个产品链接的路由,因此,在base.html模板中也需要同步的更改这三个链接。
通过使用路由传参,只需要定义一个通用的url映射入口,视图处理函数通过解析请求获得参数来渲染不同的页面。接下来编辑路由文件,打开productsApp应用中的urls.py文件,重新设置urlpatterns字段。原先的urlpatterns字段定义了三个路由,分别用作“家用机器人”、“智能监控”、“人脸识别解决方案”三个子页面的映射入口,此处由于我们已经重新将三个子页面路由进行了合并,因此,只需要定义1个路由即可,具体修改如下:
from django.urls import path
from . import views
app_name = 'productsApp'
urlpatterns = [
path('products/<str:productName>/', views.products, name='products'),
]
要从url中捕获值,需要使用尖括号<>来定义路由附带的参数。参数类型可以自行定义,在上述实例中使用了<str:productName>,表示该路由带的参数名为productName,参数类型为str字符串类型。
接下来修改views.py文件用来对该映射路由进行处理。同样的,由于目前只有1个映射路由,因此删除掉原先的三个响应函数:robot()、monitoring()、face(),然后添加1个新的带参数的处理函数,具体代码如下:
def products(request, productName):
submenu = productName
if productName == 'robot':
productName = '家用机器人'
elif productName == 'monitor':
productName = '智能监控'
else:
productName = '人脸识别解决方案'
return render(
request, 'productList.html', {
'active_menu': 'products',
'sub_menu': submenu,
'productName': productName,
})
上述代码首先定义了带参数的product函数,参数名productName与url文件中的路由参数名相同。请求经过url解析,对应的productName参数会自动传入product函数;根据传入的productName参数转换为对应的中文,并将变量传入最后的render响应函数的字典变量中。
修改完成后保存文件并启动,通过浏览器浏览页面,依次单击三个子页面查看页面切换效果。通过带参数的路由设置,实现了子页面的内容共享和切换。从本质上来说,后台渲染的只是一个页面,只不过该页面会根据不同的路由参数进行内容调整,实现效果等价于多个子页面切换。这种处理方式的好处是显而易见的,只需要编辑并维护一份HTML模板文件并且只需要处理一个视图函数即可,可以极大的提高开发效率。
效果如下图所示:
4.2 开发“产品中心”模块
4.2.1 制作产品列表页面
为了方便用户浏览产品,每个子页面均采用列表的形式展现产品。参照示例网站所示效果,列表中每一项包含1张产品照片(占6个栅格)、1行产品标题和部分文字说明(占6个栅格)。另外,为了方便排列多个产品,采用Bootstrap的分页控件将产品进行分页显示,每页最多排列3个产品。所有产品数据均以动态数据形式存放于数据库中,在页面请求时需要从后端数据库中提取指定类型的产品数据。
接下来首先创建对应的“产品”Product模型以方便对产品数据进行存储和管理。
(1)创建“产品”模型
前一节课程中创建了“荣誉”(Award)模型,通过该模型学习了图像字段models.ImageField和文字字段models.TextField的基本使用方法。但是该Award模型每条数据只包含1张图像,而本节将要创建的“产品”Product模型每条数据需要包含一张或多张图像。这在实际情况中也是比较常见的,例如一款产品可以从不同角度拍摄多张照片,这种情况下Django并没有为模型提供类似models.ImageField对应的多图字段以供使用,因此需要自行扩展数据模型字段功能。本节将使用模型之间的一对多关系:外键,来实现多图字段功能。
首先来简单解释一下数据库中的外键的基本概念。举例来说,比如现在数据库中有三个表,每个表对应Django中的一个数据模型,分别表示书、作者和出版社。1本书只能有1个出版社,1个出版社可以出版很多书,那么书和出版社的关系就是多对一。而1本书可能有多个作者,1个作者也可以出版过多本书,这两者的关系就是多对多。除了上述两种关系以外,在实际应用中还存在一对一关系。当然,可以将一对一这种模型结构完全用一个模型来表示,只需要将两个模型的所有字段合并到一个模型中,因此,这种一对一结构使用相对较少。类比到本章开发的“产品”模型,如果将模型中的图像单独拆分出去组成一个“产品图片”模型,那么1张产品图片只属于1个产品,而1个产品可以包含多张产品图片,因此这是一种多对一关系。一般情况下模型在创建时都有一个主键id号用来区分模型中的每一条数据,对于“产品”模型,使用主键id号作为唯一的区分标识。对于“产品图片”来说,可以通过定义外键的形式来表示与“产品”模型之间的多对一关系,这里的外键也就是“产品”模型的主键。
下面开始定义具体的模型,首先在productsApp应用的models.py文件中定义一个“产品”类Product:
from django.db import models
from django.utils import timezone
class Product(models.Model):
PRODUCTS_CHOICES = (
('家用机器人', '家用机器人'),
('智能监控', '智能监控'),
('人脸识别解决方案', '人脸识别解决方案'),
)
title = models.CharField(max_length=50, verbose_name=' 产品标题')
description = models.TextField(verbose_name='产品详情描述')
productType = models.CharField(choices=PRODUCTS_CHOICES,
max_length=50,
verbose_name='产品类型')
price = models.DecimalField(max_digits=7,
decimal_places=1,
blank=True,
null=True,
verbose_name='产品价格')
publishDate = models.DateTimeField(max_length=20,
default=timezone.now,
verbose_name='发布时间')
views = models.PositiveIntegerField('浏览量', default=0)
def __str__(self):
return self.title
class Meta:
verbose_name = '产品'
verbose_name_plural = '产品'
ordering = ('-publishDate', )
上述模型除了图片以外对产品的多个字段进行了定义,下面逐个来分析:
- title:产品的标题。它对应字符类型字段CharField,其最大长度限定在50以内;
- description:产品的详细描述。它对应文本字段TextField;
- productType:产品类型。对应字符字段CharField。在本章开发案例中,产品共分为三种类型,每种类型的定义在PRODUCTS_CHOICES中给出。PRODUCTS_CHOICES是一个元组,在定义productType字段时通过参数choices传入使用;
- price:产品价格。对应小数字段DecimalField,通过设置blank=True和null=True表示该字段允许为空;
- publishDate:对应时间字段DateTimeField,该字段表明该产品的发布时间。这里使用timezone.now方法来设置默认值为当前时间;
- views:对应整数字段PositiveIntegerField,该字段用来记录该产品页面被浏览的次数;
除了上述字段以外,在Product模型中额外定义了一个__str__函数,该函数以self为参数,这里的self即为自身,返回值为当前模型数据的title字段。该函数的作用是使得该模型的每条数据在后台管理系统列表中均以每条数据的title字段来显示。在元信息类Meta中,通过定义verbose_name和verbose_name_plural来规范Product模型在后台管理系统中的显示名。这里值得注意的是ordering属性,该属性中填入了带负号的publishDate,这样在后台管理系统中产品将按照产品发布时间由近到远来显示。
接下来定义一个“产品图片”类ProductImg,该类从属于产品类,其定义如下:
class ProductImg(models.Model):
product = models.ForeignKey(Product,
related_name='productImgs',
verbose_name='产品',
on_delete=models.CASCADE)
photo = models.ImageField(upload_to='Product/',
blank=True,
verbose_name='产品图片')
class Meta:
verbose_name = '产品图片'
verbose_name_plural = '产品图片'
该类有两个字段:
- product:产品字段,这是一个外键,用models.ForeignKey来声明,该外键第一个参数用于指明从属的类,此处为产品类Product。另外一个参数related_name用来声明逆向名称,也就是说如果需要在Product类中调用产品图片可以采用该名称进行调用,具体调用示例会在后续章节中再详细阐述。在末尾添加了on_delete参数,这是在Django 2.0版本后,为了避免两个表里的数据不一致问题而需要额外设置的;
- photo:该字段与“荣誉”模型Award中的photo字段一样,用来存储上传的产品图片。其根路径由settings.py中的media参数设置,而子路径由upload_to参数指定;
定义完上述模型以后,为了能够方便管理员管理模型,需要将上述模型添加到admin管理模块中。具体的,编辑productsApp下的admin.py文件,代码如下:
from django.contrib import admin
from .models import Product,ProductImg
admin.site.register(Product)
admin.site.register(ProductImg)
首先从当前models.py文件中导入前面创建的Product和ProductImg模型,然后通过admin.site.register()函数分别将两模型进行注册。
至此,模型创建工作已经完成,接下来需要同步模型到数据库中。在终端中依次输入下述命令完成数据模型同步工作:
python manage.py makemigrations
python manage.py migrate
保存所有修改后重新启动项目,进入管理后台,可以发现当前管理系统后台中已经集成了“产品”和“产品图片”两个模型,如下图所示:
本小节完成了“产品”模型的创建,为了让每条产品数据能够包含多张图片,本节额外的创建了ProductImg模型,该模型通过外键附属于Product模型。接下来将学习如何在后台管理系统中管理多对一模型。?
由于“产品”和“产品图片”模型具有一对多关系,一般情况下先添加产品,然后再逐条添加产品图片,这样在添加产品图片时只需要指明对应的所属产品即可。
首先在“产品”模型右侧单击“增加”按钮,按照表单提示添加一条数据:
?这里注意,其中“产品价格”字段对应Product模型中的price,该字段在定义时允许为空,因此在添加数据时呈现灰色,即可以不添加。“产品类型”字段由于使用了choices参数,因此默认的表单输入控件使用了带下拉按钮的选择框。“发布时间”字段默认以当前时间填入,因此可以不作处理。编辑完成后单击右侧保存按钮即可完成一条数据的添加。
?接下来为该条产品数据添加产品图片。按照同样的方法,进入“产品图片”添加页面。产品图片中的两个字段与模型构建时设计的product和photo字段一一对应。这里注意到,“产品”字段使用了下拉菜单来填写信息。单击下拉菜单可以看到之前已经增加的产品数据,也就是说后台管理系统通过模型的外键已经自动的为数据创建了关联,这里只需要选择图片所属的产品即可。最后,通过“产品图片”上的浏览按钮上传1张照片,单击保存按钮即可完成1条“产品图片”数据的添加。按照该步骤可以继续为同一款产品数据添加多个产品图片。
这里需要注意的是,由于模型之间的关联性,当删除“产品”模型中的某一条数据时,会同时删除其包含的所有“产品图片”数据。
根据上述设置,通过后台管理系统分别向“家用机器人”、“智能监控”、“人脸识别解决方案”三大类产品添加多条数据信息以方便后续对模型数据的处理演示。
(2)模型数据过滤、排序和渲染
本小节实现“产品”模型的数据读取、过滤、排序、页面渲染等操作。在读取模型数据时,需要根据不同的产品类型提取不同的数据,因此需要实现数据的过滤功能。另外,在实际使用时希望最新发布的产品能够显示在页面最前端,也就是说在数据提取时需要按照时间进行排序。除了上述操作以外,多对一模型如何在模板中进行数据渲染也是本小节需要重点掌握的内容。
Django为模型提供了方便的数据过滤和排序功能,其核心函数分别为filter和order_by。打开productsApp中的views.py文件,继续编辑其中的products函数,在获取当前请求后根据解析得到的产品类型从数据库中提取数据,并进行排序,完整代码如下:
from django.shortcuts import render
from .models import Product
def products(request, productName):
submenu = productName
if productName == 'robot':
productName = '家用机器人'
elif productName == 'monitor':
productName = '智能监控'
else:
productName = '人脸识别解决方案'
productList = Product.objects.all().filter(
productType=productName).order_by('-publishDate')
return render(
request, 'productList.html', {
'active_menu': 'products',
'sub_menu': submenu,
'productName': productName,
'productList': productList,
})
- 上述代码首先解析参数productName并进行转换,然后通过Product.objects.all()获取所有的产品数据;
- 紧接着使用filter函数进行过滤,该函数是Django提供的数据过滤函数,可以对提取到的数据查询集按照指定条件进行过滤,此处指定的过滤条件为productType=productName,即找到所有产品模型Product中productType字段为productName的产品并将其提取出来;
- 接下来在提取到的新的查询子集上采用order_by函数进行数据排序。排序方式以产品模型的publishDate字段为依据。此处在publishDate前的负号表示按照时间由近到远排序,反之不加负号表示由远到近排序;
- 最后的render函数添加新的变量productList用于将查询到的数据集变量添加到模板中进行渲染;
下面需要设计产品列表的页面主体,然后实现数据模型的渲染。参照示例网站所示页面效果,在产品列表部分采用左右对称方式排列产品内容。左侧显示产品图像,右侧显示产品标题和详细内容。这里注意两点,由于一款产品可以包含多幅产品图像,这里为了方便,仅显示每款产品的第一幅图像。右侧采用了Bootstrap的缩略图组件来排列产品文字信息。由于产品的描述信息可能比较长,如果将其全部显示那么容易超出左侧图片高度影响页面的浏览体验,比较好的方式就是对产品描述文字进行字数限制,超过部分不再显示。打开productList.html文件,在页面主体<div class="model-details">标签内添加代码如下:
{% for product in productList %}
<div class="row">
<div class="col-md-6">
{% for img in product.productImgs.all %}
{% if forloop.first %}
<a href="#" class="thumbnail row-4">
<img class="img-responsive model-img" src="{{img.photo.url}}">
</a>
{% endif %}
{% endfor %}
</div>
<div class="col-md-6">
<h3>{{ product.title|truncatechars:"20" }}</h3>
<p>{{ product.description|truncatechars:"150"|linebreaks }}</p>
<div class="thumbnail row-5">
<div class="caption">
<a href="#" class="btn btn-primary" role="button">
查看详情
</a>
</div>
</div>
</div>
</div>
{% endfor %}
- 上述代码首先采用模板标签{% for product in productList %}{% endfor %}来遍历传入的产品列表变量productList,此时遍历的每个产品赋值到新变量product中;
- 在我们定义的“产品—产品图片”模型中,采用的是“一对多”关系结构。为了能够从新变量product中取出对应的“产品图片”中的第一幅图像,需要使用ProductImg模型中product字段的related_name参数,通过该参数来获取对应的产品图片列表。对应上述代码{% for img in product.productImgs.all %};
- 接下来采用了模板标签{% if forloop.first %}来判断当前是否遍历到第一张产品图片,如果是则取出图片url并在<img>中进行显示;
- 产品文字部分采用{{ product.title}}模板变量来实现,其后紧跟的truncatechars为Django提供的模板过滤器,可以对文字进行截断显示。此处对应的product.title截断字数为20,产品文字描述{{ product.description }}截断字数为150。Django提供了多达30多种的过滤器可供使用,各个过滤器可以级联使用。例如在本例的产品文字描述部分,不仅使用了truncatechars过滤器,同时也使用了linebreaks过滤器,使得文字的换行能够有效的在HTML中实现。
最后对页面列表的样式做一些美化,调整间距和边框。由于这些美化样式是专门针对产品列表页面的,因此最好不要将这些css样式代码写到全局的style.css文件中,而是独立使用一个样式文件。具体的,在style.css同目录下创建一个products.css文件,在products.css文件中添加如下代码
.model-details .row-4{
Margin-top: 20px;
}
.model-details .row-5{
border:none;
}
然后在productList.html文件的{% block content %}内,引入products.css文件:
<link href="{% static 'css/products.css' %}" rel="stylesheet">
最终效果如下图所示:
4.2.2 分页显示
前面已基本完成了产品列表的页面制作,实现了产品模型的创建、管理、查询、过滤、排序和页面渲染,所有产品数据在单一页面显示,用户可以通过浏览器滚动条进行翻滚以浏览产品。很明显,当商品数量较多时这种浏览方式显得不够直观、不方便。一种比较好的解决方法就是将所有产品分布到不同的页面,每个页面只允许显示固定数量的产品,这种方式方便页面布局也方便用户快速定位浏览。
Django提供了一个分页类Paginator来帮助管理分页数据,这个类存放在django/core/paginator.py文件中,它可以接收列表、元组或其它可迭代的对象。为了能够正常使用该分页类,需要在使用文件中引入分页对象,具体的在productsApp的views.py文件中添加下面的代码:
from django.core.paginator import Paginator
下来就可以在product函数中使用该分页对象。具体操作时将数据库中取出的产品数据productList直接作为分页类的参数来创建分页对象,然后通过该分页对象来控制页面的切换,详细代码如下:
from django.shortcuts import render
from .models import Product
from django.core.paginator import Paginator
def products(request, productName):
submenu = productName
if productName == 'robot':
productName = '家用机器人'
elif productName == 'monitor':
productName = '智能监控'
else:
productName = '人脸识别解决方案'
productList = Product.objects.all().filter(
productType=productName).order_by('-publishDate')
p = Paginator(productList,2)
if p.num_pages <= 1:
pageData = ''
else:
page = int(request.GET.get('page',1))
productList = p.page(page)
left = []
right = []
left_has_more = False
right_has_more = False
first = False
last = False
total_pages = p.num_pages
page_range = p.page_range
if page == 1:
right = page_range[page:page+2]
print(total_pages)
if right[-1] < total_pages - 1:
right_has_more = True
if right[-1] < total_pages:
last = True
elif page == total_pages:
left = page_range[(page-3) if (page-3) > 0 else 0:page-1]
if left[0] > 2:
left_has_more = True
if left[0] > 1:
first = True
else:
left = page_range[(page-3) if (page-3) > 0 else 0:page-1]
right = page_range[page:page+2]
if left[0] > 2:
left_has_more = True
if left[0] > 1:
first = True
if right[-1] < total_pages - 1:
right_has_more = True
if right[-1] < total_pages:
last = True
pageData = {
'left':left,
'right':right,
'left_has_more':left_has_more,
'right_has_more':right_has_more,
'first':first,
'last':last,
'total_pages':total_pages,
'page':page,
}
return render(
request, 'productList.html', {
'active_menu': 'products',
'sub_menu': submenu,
'productName': productName,
'productList': productList,
'pageData': pageData,
})
- 上述代码对原始的查询集productList进行整理,按照页数进行拆分,然后找到对应页数的数据。在具体实现上,为能够动态的变换页面按钮,在逻辑上进行了多种判断和处理,这段代码读者可以对照最终的实现效果逐步分析;
- 页数的获取方式是通过代码page = int(request.GET.get('page',1))来得到。如果说 urls.py文件是Django中前端页面和后台程序的桥梁,那么request就是桥上负责运输的小汽车,后端接收到前端的信息几乎全部来自于requests。本案例的分页制作也是基于此原理,用户在产品列表上翻页时会通过浏览器将翻页的页数以参数形式封装到request里,然后传入后端,后端通过request.GET.get来得到指定的参数;
- 最后将有关分页的一些关键数据以字典“键—值”对形式存于变量pageData中;
- 在最终渲染时,只需要额外的添加变量pageData即可;
编辑完后台视图处理函数以后,接下来开始编辑前端页面文件productList.html以实现产品的分页浏览。这里采用了Bootstrap的分页控件,该控件采用一个无序列表<ul>来实现,对应的class类为pagination。修改productList.html文件,在完成列表显示以后添加分页控件,并编写分页控件各个按钮逻辑,详细代码如下:
{% if pageData %}
<div class="paging">
<ul id="pages" class="pagination pagination-sm pagination-xs">
{% if pageData.first %}
<li><a href="?page=1">1</a></li>
{% endif %}
{% if pageData.left %}
{% if pageData.left_has_more %}
<li><span>...</span></li>
{% endif %}
{% for i in pageData.left %}
<li><a href="?page={{i}}">{{i}}</a></li>
{% endfor %}
{% endif %}
<li class="active"><a href="?page={{pageData.page}}">{{pageData.page}}</a></li>
{% if pageData.right %}
{% for i in pageData.right %}
<li><a href="?page={{i}}">{{i}}</a></li>
{% endfor %}
{% if pageData.right_has_more %}
<li><span>...</span></li>
{% endif %}
{% endif %}
{% if pageData.last %}
<li><a href="?page={{pageData.total_pages}}">{{pageData.total_pages}}</a></li>
{% endif %}
</ul>
</div>
{% endif %}
- 上述代码通过传入的pageData变量来实现分页。由于当前分页数据以2条数据为1页,因此当产品数量不足2条时不显示分页控件,这个通过代码{% if pageData %}来实现。当产品数量超过2个以上时,就需要分页控件;
- 分页控件本质上是由指定数量的链接<a>组成,每个链接采用类似<a href="?page=3">的语句来链接到指定页数的列表页。这里注意带参数的访问网址基本写法,用户在单击该链接时会将参数page封装到request中,后台会从request中解析到page变量值;
分页控件的使用可以改善用户的浏览体验,为了使分页控件功能更加丰富,上述代码在实现时相对较为复杂,需要结合后端代码仔细推敲。读者可以先自行尝试翻页效果,然后对照本课程实例代码进行分析和学习。
最后为了页面美观,对分页控件作一些样式调整,使其居中显示并且对激活状态的链接设置背景色和边框色。编辑products.css文件,添加代码如下:
.paging{
text-align:center;
}
.pagination .active a{
background-color:#005197;
border-color:#005197;
}
保存所有修改后启动项目,查看页面访问效果,如下图所示:
4.2.3 制作产品详情页面
“产品详情”页面附属于“产品列表”页面,用户在浏览产品列表时单击某一产品即可进入“产品详情”页面查看该款产品的详细介绍,具体包括产品图片、文字介绍和价格等信息。 下面梳理一下实现上述功能的基本流程: (1)用户在产品列表中单击某一产品链接,该链接包含产品的唯一id号作为附带参数; (2)服务器接收请求,通过定义好的url调用带参数的视图函数进行处理; (3)视图函数根据传入的id参数从数据库中查找对应的产品,找到产品后通过render函数返回产品内容给客户端; 在具体操作前,先分析一下其中实现的难点。在用户单击产品链接的时候需要能够准确的获取产品id号,并将其作为参数附带在访问网址中。在制作产品列表页面时,已经使用过带参数的路径访问,例如在访问“家用机器人”列表页面时采用了下面的访问形式:
"{% url 'productsApp:products' 'robot' %}"
可以发现,这里的参数即为字符串'robot',该参数是通过硬编码的方式直接写入到html文件中,因此该链接的参数是无法根据实际情况动态变化的。而针对产品详情页面,所附带的参数是需要根据产品id号动态变更的。因此其解决方案就是其链接附带的参数也通过模板变量来替代,而模板变量的值即为产品id号,对应的链接形式如下:
"{% url 'productsApp:productDetail' product.id %}"
将上述访问路径替换到productList.html产品列表图片和“查看详情”按钮的href属性中,使得用户无论单击产品图片还是单击“查看详情”按钮都可以进入到该产品的详情页面。
下面开始开发具体的产品详情页面。首先编辑productsApp应用中的urls.py文件,在urlpatterns字段中为产品详情页面添加一条新的路由:
urlpatterns = [
path('products/<str:productName>/', views.products, name='products'),
path('productDetail/<int:id>/', views.productDetail, name='productDetail'), #添加产品详情路由
]
上述路由附带一个int型参数,参数名为id。该路由映射到视图中的productDetail函数。下面打开views.py文件,添加productDetail函数如下:
from django.shortcuts import get_object_or_404
def productDetail(request, id):
product = get_object_or_404(Product, id=id)
product.views += 1
product.save()
return render(request, 'productDetail.html', {
'active_menu': 'products',
'product': product,
})
- 上述代码首先引入get_object_or_404函数,该函数可以根据模型id号查找指定的产品数据,如果查找不到则会返回404错误。视图函数productDetail根据前面定义的路由附带参数id,然后在函数实现部分通过get_object_or_404函数查找到指定id号的产品并由render函数返回给前端;
- 找到指定id的产品后,需要同步更新该款产品的访问量views,并且通过product.save()函数将改动保存到数据库中;
接下来开始设计“产品详情”页面。根据示例网站所示页面效果,产品详情页面并没有广告横幅,这主要是为了突出产品本身的内容,可以将用户注意力更多的保留在产品本身。产品详情主体部分主要包括产品图片、产品完整描述信息和产品参考价格三部分,其中产品图片以堆叠的方式显示在主体头部。由于视图处理函数已经传回了当前需要渲染的product变量,因此只需要在产品详情页中调用product相关字段即可。
在productsApp/templates文件夹中新增productDetail.html文件,编辑代码如下:
{% extends "base.html" %}
{% load static %}
{% block title %}
产品详情
{% endblock %}
{% block content %}
<link href="{% static 'css/products.css' %}" rel="stylesheet">
<!-- 主体内容 -->
<div class="container">
<div class="model-details-product-title">
{{product.title}}
</div>
<div class="model-details">
{% for img in product.productImgs.all %}
<div class="row-4">
<img class="img-responsive" src="{{img.photo.url}}">
</div>
{% endfor %}
<h3>产品介绍</h3>
<p>
{{product.description|linebreaks}}
</p>
<h3>参考价格</h3>
<p>
{{product.price}}元
</p>
</div>
</div>
{% endblock %}
另外,编辑products.css文件,添加样式代码:
.model-details-product-title{
padding:15px 0px;
font-size:18px;
border-bottom:1px #005197 solid;
color:#005197;
margin-bottom:10px;
margin-top:10px;
text-align:center;
}
保存修改后,运行项目,单击任一一款产品,可以进入产品详情页面,如下图所示:
到这里"产品中心模块"就开发完成了。
4.3 富文本的概念
4.3.1 创建基于富文本的新闻模型
在开发产品模块时,后台需要先创建一条包含图片和文字描述的产品数据,然后再将产品文字和图片渲染在前端浏览器上。这些文字和图片在前端显示的时候往往没有格式或者格式是固定的,并且这些数据需要采用模型外键关联的方式进行管理,这种处理方式不方便也不直观,渲染的界面比较死板,管理员无法个性化的定制页面。富文本编辑器就是为了解决这个问题而产生的。
富文本编辑器(Rich Text Editor, RTE)是一种可内嵌于浏览器、所见即所得的文本编辑器。富文本编辑器不同于普通的文本编辑器,富文本编辑器可以方便的内嵌于Web应用中以方便用户编辑文章或信息。简单来说,以Django项目为例,通过富文本的导入,可以让用户在后台管理系统中像使用Word一样编辑文章,而文章在前端最终的显示形式与编辑的时候一致,用户不用再去管理繁杂的数据存储机制,仅需简单的配置接口和路径即可完成上述功能。目前,有很多有名的富文本编辑器,比如国外的有TinyMCE、Ckeditor、bootstrap-wysiwyg、kindeditor等,而国内的则有Ueditor、kindeditor和LayEdit等。总体来说,国内的富文本编辑器在编辑风格方面相对国外具有更大的天然优势,对中文网站的支持更加友好,其中以百度的Ueditor知名度较高。
Ueditor是由百度Web前端研发部开发的所见即所得富文本编辑器,具有轻量、可定制、注重用户体验等特点,基于开源MIT协议,允许自由使用和修改。接下来我们将集成Ueditor到hengDaProject项目中,实现用户的个性化新闻页面编辑功能。下面进入具体的开发环节。
首先下载现成的Ueditor项目,下载网址:https://github.com/twz915/DjangoUeditor3/。解压后在本地安装,需要在终端中通过cd命令定位到DjangoUeditor3-master根文件夹下面(与setup.py同级目录),然后输入下述命令即可完成安装:
python setup.py install
安装完成后为了能够在自己的Django项目中集成Ueditor编辑器,需要将DjangoUeditor3-master项目中的DjangoUeditor文件夹拷贝到当前项目中。从本质上来说DjangoUeditor即为一个Django应用,接下来只需要将该应用添加到我们的hengDaProject项目中即可。将DjangoUeditor文件夹复制到hengDaProject项目的根目录下,然后打开配置文件settings.py文件,将DjangoUeditor应用添加到项目中:
INSTALLED_APPS = [
...其它应用...
'DjangoUeditor', # 添加富文本应用
]
然后对添加的DjangoUeditor应用进行路由配置,打开配置文件夹hengDaProject下的urls.py文件,在urlpatterns字段中添加DjangoUeditor应用对应的路由:
path('ueditor/',include('DjangoUeditor.urls')),
这个时候运行代码会发现出现下面的错误:
ModuleNotFoundError: No module named 'django.utils.six'
这是因为我们使用的是最新的django3,django3已经移除了six。我们可以单独安装它:
pip install six
安装完six之后,将DjangoUEditor中有关的引用路径修改一下。具体修改如下:
(1)DjangoUEditor目录下的views.py文件中有如下行
#from django.utils import six
from django.utils.six.moves.urllib.request import urlopen
from django.utils.six.moves.urllib.parse import urljoin
改为:
import six
from urllib.request import urlopen
from urllib.parse import urljoin
(2)widgets.py文件中有如下一行:
from django.utils.six import string_types
修改为:
from six import string_types
(3)urllib也已从six之中独立出来,故修改commands.py文件中的如下一行:
from django.utils.six.moves.urllib.parse import urljoin
改为:
from urllib.parse import urljoin
(4)utils.py文件中有如下一行:
from django.utils import six
修改为:
import six
修改完成后重新启动项目,如果不报错说明Ueditor安装成功。
在创建“新闻”模型前,我们先分析该模型需要创建的字段。参考前面创建的“产品”模型,“新闻”模型同样需要标题(title)和详细内容(description)字段,其中标题是字符型数据,而详细内容则可以包含文字、图片、文件下载链接等,并且可以任意排布这些内容元素,该字段需要使用富文本来实现。除了上述两个字段以外还需要添加新闻类型、发布时间、浏览量等字段,这些字段可以采用Django的常用模型字段来实现。
具体的,打开newsApp文件夹下面的models.py文件,在该文件中创建“新闻”模型:
from django.db import models
from DjangoUeditor.models import UEditorField
import django.utils.timezone as timezone
class MyNew(models.Model):
NEWS_CHOICES = (
('企业要闻', '企业要闻'),
('行业新闻', '行业新闻'),
('通知公告', '通知公告'),
)
title = models.CharField(max_length=50, verbose_name=' 新闻标题')
description = UEditorField(u'内容',
default='',
width=1000,
height=300,
imagePath='news/images/',
filePath='news/files/')
newType = models.CharField(choices=NEWS_CHOICES,
max_length=50,
verbose_name='新闻类型')
publishDate = models.DateTimeField(max_length=20,
default=timezone.now,
verbose_name='发布时间')
views = models.PositiveIntegerField('浏览量', default=0)
def __str__(self):
return self.title
class Meta:
ordering = ['-publishDate']
verbose_name = "新闻"
verbose_name_plural = verbose_name
- 上述代码首先引入所需的模块,其中尤其需要注意富文本模块DjangoUeditor.models中UEditorField的导入,这样可以在模型中使用UEditorField来创建富文本字段从而可以方便的嵌入各种文本、图像、链接元素;
- 接下来创建MyNew类。根据之前的分析,MyNew类包含新闻标题title、内容描述description、新闻类型newType、发布时间publishDate和浏览量views字段。各字段除了description以外均采用了django.db默认提供的常规数据字段,读者可以参考第5章中表5.1列出的常规模型字段来查看各字段具体含义和使用方法。由于需要对新闻按内容进行划分,因此参考第5章中“产品类型”的设计方式将新闻类型字段newType通过传入choices参数来设置类型;
- Description字段使用了富文本UEditorField来申明,其中u'内容'用来定义该字段在后台管理系统中的别名。width=1000和height=300表示后台管理系统中该字段最后的编辑界面宽度为1000像素,长度为300像素。imagePath和filePath分别用来指明用户上传的图像和文件最终的存储目录。这里注意,imagePath和filePath参数的使用需要依赖项目配置文件settings.py中MEDIA_URL和MEDIA_ROOT的配置。对于本书实例,最终的图像和文件的输出目录会在media文件夹中;
- 除了上述模型字段以外,MyNew模型还通过定义def __str__(self)函数来设置后台管理系统中新闻列表每条新闻的显示名称。通过定义Meta类来申明模型数据的排序方式(按照发布时间进行排序,注意负号的作用)以及模型在后台管理系统中的别名;
创建完模型以后在命令终端中进行模型数据迁移完成数据库同步:
python manage.py makemigrations
python manage.py migrate
为了能够在后台管理系统中使用前面创建的MyNew模型,需要在admin中进行模型注册。打开newsApp应用中的admin.py文件,对创建的MyNew模型进行注册,具体代码如下:
from django.contrib import admin
from .models import MyNew
class MyNewAdmin(admin.ModelAdmin):
style_fields={'description':'ueditor'}
admin.site.register(MyNew, MyNewAdmin)
上述代码需要注意富文本字段的注册形式,通过定义style_fields属性来绑定富文本字段。
打开后台管理系统,在newsApp模块中添加一条记录,可以看到富文本编辑器已经自动添加到对应的界面中,其使用方式与Microsoft Office Word类似,允许管理人员自己编辑页面内容,包括文字、图片等,并且允许对这些内容元素样式和位置进行修改。读者可以自行尝试使用Ueditor编辑器进行新闻内容编辑,使用相对简单,本课程不再对其进行介绍。
?这里有一个需要注意,如果要上传图片,那么需要使用ueditor的“多图上传功能”,先上传图片,再点击“开始上传”,最后确认即可。
4.3.2 开发新闻列表和新闻详情页面
本小节开始开发“新闻列表”和“新闻详情”页面。开发流程和实现方法与前面开发的“产品列表”和“产品详情”基本相似,区别在于前端界面的设计以及最终富文本的渲染方式不同。本节对差异部分将重点阐述,而与前面类似的地方则仅给出重要的代码,不再深入分析,读者可以参考本课程代码实例查看详细内容。 具体的,新闻模块整体访问流程如下: (1)用户单击“企业要闻”、“行业新闻”、“通知公告”任一子页面产生请求,通过浏览器将请求发送至服务器; (2)服务器采用统一的路由进行映射,匹配指定的视图函数进行处理; (3)视图函数根据请求附带的参数来确定请求的子页面类型,通过ORM操作来过滤、查询数据并返回页面; (4)前端收到返回的页面内容进行输出,其中富文本内容按照编辑时的样式进行渲染。
“新闻列表”对应的视图处理函数主要完成数据的读取任务,其中为了方便浏览需要使用分页组件进行分页处理。参照前面“产品列表”对应的视图函数,将products函数复制到“新闻列表”中,修改函数名为news,然后同步修改对应的参数名称即可。详细代码如下:
from django.shortcuts import render
from .models import MyNew
from django.core.paginator import Paginator
def news(request, newName):
# 解析请求的新闻类型
submenu = newName
if newName == 'company':
newName = '企业要闻'
elif newName == 'industry':
newName = '行业新闻'
else:
newName = '通知公告'
# 从数据库获取、过滤和排序数据
newList= MyNew.objects.all().filter(newType = newName).order_by('-publishDate')
# 分页
p = Paginator(newList, 5)
if p.num_pages <= 1:
pageData = ''
else:
page = int(request.GET.get('page', 1))
newList = p.page(page)
left = []
right = []
left_has_more = False
right_has_more = False
first = False
last = False
total_pages = p.num_pages
page_range = p.page_range
if page == 1:
right = page_range[page:page + 2]
print(total_pages)
if right[-1] < total_pages - 1:
right_has_more = True
if right[-1] < total_pages:
last = True
elif page == total_pages:
left = page_range[(page - 3) if (page - 3) > 0 else 0:page - 1]
if left[0] > 2:
left_has_more = True
if left[0] > 1:
first = True
else:
left = page_range[(page - 3) if (page - 3) > 0 else 0:page - 1]
right = page_range[page:page + 2]
if left[0] > 2:
left_has_more = True
if left[0] > 1:
first = True
if right[-1] < total_pages - 1:
right_has_more = True
if right[-1] < total_pages:
last = True
pageData = {
'left': left,
'right': right,
'left_has_more': left_has_more,
'right_has_more': right_has_more,
'first': first,
'last': last,
'total_pages': total_pages,
'page': page,
}
return render(
request, 'newList.html', {
'active_menu': 'news',
'sub_menu': submenu,
'newName': newName,
'newList': newList,
'pageData': pageData,
})
接下来修改对应的路由。打开newsApp应用下的urls.py文件,由于新闻动态中的三个子页面共享同一个路由,因此删除原先设计的路由,重新编辑urlpatterns字段如下:
urlpatterns = [
path('news/<str:newName>/', views.news, name='news'),#新闻列表
]
自此,已完成新闻列表的视图函数处理部分以及对应路由的设置。由于修改了新闻动态各子页面的路由形式,因此需要修改基础模板base.html中新闻动态各子页面的访问路径。打开base.html文件,修改“新闻动态”部分代码:
<li><a href="{% url 'newsApp:news' 'company' %}">企业要闻</a></li>
<li><a href="{% url 'newsApp:news' 'industry' %}">行业新闻</a></li>
<li><a href="{% url 'newsApp:news' 'notice' %}">>通知公告</a></li>
完成上述设置后开始创建并编辑“新闻列表”页面。
在newsApp应用中创建templates文件夹,然后在该文件夹下创建newList.html文件。编辑newList.html文件,详细代码如下:
{% extends "base.html" %}
{% load static %}
{% block title %}
{{newName}}
{% endblock %}
{% block content %}
<!-- 广告横幅 -->
<div class="container-fluid">
<div class="row">
<img class="img-responsive model-img" src="{% static 'img/new.jpg' %}">
</div>
</div>
<!-- 主体内容 -->
<div class="container">
<div class="row row-3">
<!-- 侧边导航栏 -->
<div class="col-md-3">
<div class="model-title">
新闻动态
</div>
<div class="model-list">
<ul class="list-group">
<li class="list-group-item" id='company'>
<a href="{% url 'newsApp:news' 'company' %}">企业要闻</a>
</li>
<li class="list-group-item" id='industry'>
<a href="{% url 'newsApp:news' 'industry' %}">行业新闻</a>
</li>
<li class="list-group-item" id='notice'>
<a href="{% url 'newsApp:news' 'notice' %}">通知公告</a>
</li>
</ul>
</div>
</div>
<!-- 说明文字和图片 -->
<div class="col-md-9">
<div class="model-details-title">
{{newName}}
</div>
<div class="model-details">
{% for mynew in newList %}
<div class="news-model">
<img src="{% static 'img/newsicon.gif' %}">
<a href="#"><b>{{mynew.title}}</b></a><span>【{{mynew.publishDate|date:"Y-m-d"}}】</span>
<p>
<!-- 添加新闻简要说明 -->
</p>
</div>
{% endfor %}
{% if pageData %}
<div class="paging">
<ul id="pages" class="pagination">
{% if pageData.first %}
<li><a href="?page=1">1</a></li>
{% endif %}
{% if pageData.left %}
{% if pageData.left_has_more %}
<li><span>...</span></li>
{% endif %}
{% for i in pageData.left %}
<li><a href="?page={{i}}">{{i}}</a></li>
{% endfor %}
{% endif %}
<li class="active"><a href="?page={{pageData.page}}">{{pageData.page}}</a></li>
{% if pageData.right %}
{% for i in pageData.right %}
<li><a href="?page={{i}}">{{i}}</a></li>
{% endfor %}
{% if pageData.right_has_more %}
<li><span>...</span></li>
{% endif %}
{% endif %}
{% if pageData.last %}
<li><a href="?page={{pageData.total_pages}}">{{pageData.total_pages}}</a></li>
{% endif %}
</ul>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
- 上述代码与“产品中心”模块productList.html内容大致相同,只是在主体渲染部分没有采用Bootstrap的缩略图组件,而是采用了一个class="news-model"的<div>类来进行新闻列表显示,该样式类的定义将在后面给出;
- 在newList.html文件中新闻日期在输出时采用了模板过滤器date:"Y-m-d"来格式化显示。在新闻列表的每个<div>内还包含了一个<p>标签用于显示新闻的简要介绍,也就是将每条新闻的文字部分提取出一部分进行显示;
- 由于新闻模型采用了富文本进行内容绑定,而富文本本身是以格式化的HTML字符串存储在数据库中,因此需要借助第三方HTML字符串解析工具来对新闻内容解析以获得页面元素,这部分内容将在后面进行介绍,本小节可以先暂时不作处理。
类似于“产品列表”页面,为了定制化“新闻列表”样式,在style.css文件的同目录下创建news.css文件,在该文件中添加样式定义
/* 分页控件样式 */
.paging{
text-align:center;
}
.pagination .active a{
background-color:#005197;
border-color:#005197;
}
/* 新闻列表样式 */
.news-model{
margin-top:15px;
}
.news-model span{
float:right;
}
.news-model a{
color:#666;
font-size:16px;
}
.news-model a:hover, .news-model a:focus{
text-decoration:none;
color:#d30a1c;
}
.news-model p{
margin-top:5px;
font-size:13px;
}
最后,在newList.html文件的{% block content %}内添加样式引用:
<link href="{% static 'css/news.css' %}" rel="stylesheet">
保存所有修改后启动项目,“新闻列表”初始效果图如下图所示:
?“新闻详情”页面的渲染主要通过在“新闻列表”页面为每条新闻链接绑定id号来实现。后台视图处理函数解析该id号然后从数据库中获取数据再返回页面内容。同样,参照“产品详情”视图处理函数实现方法,在newsApp的views函数中添加“新闻详情”视图处理函数,代码如下:
from django.shortcuts import get_object_or_404
def newDetail(request, id):
mynew = get_object_or_404(MyNew, id=id)
mynew.views += 1
mynew.save()
return render(request, 'newDetail.html', {
'active_menu': 'news',
'mynew': mynew,
})
其中mynew.views+=1表示每次页面访问的时候浏览次数累计加1,从而方便统计每条新闻的浏览次数。mynew.save()表示将数据的更改保存到数据库中。
接下来修改newsApp应用下的urls.py文件,为newDetail绑定对应的路由。在urlpatterns字段中添加路由:
path('newDetail/<int:id>/', views.newDetail, name='newDetail'),
最后修改newList.html文件中每一条新闻的访问路径,将:
<a href="#"><b>{{mynew.title}}</b></a>
修改为:
<a href="{% url 'newsApp:newDetail' mynew.id %}"><b>{{mynew.title}}</b></a>
这样就可以将每条新闻的id作为参数动态的绑定到访问路径中,通过使用逆向解析的方式得到每条新闻的真实url。
最后我们参照前面“产品详情”页面的设计方式,将其沿用到“新闻详情”页面。在newsApp应用的templates文件夹下创建一个newDetail.html文件,添加代码如下:
{% extends "base.html" %}
{% load static %}
{% block title %}
新闻详情
{% endblock %}
{% block content %}
<link href="{% static 'css/news.css' %}" rel="stylesheet">
<!-- 主体内容 -->
<div class="container">
<div class="model-details-product-title">
{{mynew.title}}
<div class="model-foot">发布时间:{{mynew.publishDate|date:"Y-m-d"}} 浏览次数:{{mynew.views}}</div>
</div>
<div class="model-details">
{{ mynew.description | safe }}
</div>
</div>
{% endblock %}
上述代码在主体标题部分通过传入的模板变量mynew来渲染“发布日期”和“浏览次数”。在主体描述部分则采用了{{ mynew.description | safe }}来实现富文本的内容渲染,此时Django模板标签会自动的解析富文本中的内容,并将其按照特定的格式进行展现。最后,在news.css文件中添加相关样式:
/* 新闻详情 */
.model-details-product-title{
padding:15px 0px;
font-size:18px;
border-bottom:1px #005197 solid;
color:#005197;
margin-bottom:10px;
margin-top:10px;
text-align:center;
}
/* 新闻主体副标题 */
.model-foot{
padding:5px 0px;
font-size:14px;
color:#545353;
margin-top:10px;
text-align:center;
}
保存所有修改后启动项目,将“新闻列表”和“新闻详情”页面串联起来进行测试,在“新闻列表”页面单击任一一条新闻进入“新闻详情”页面,查看跳转逻辑和显示是否正常。
正常情况“新闻详情”页面如下图所示:
?到这里我们就完成了本节课程的所有内容。
|