本教程内容已过时,更新版教程请访问: Django 博客开发入门教程。
这是 Django 博客教程的第 9 篇,在阅读此篇教程以前,请确保你已阅读 Django 博客教程的前 8 篇:
1. Django 博客教程:前言
2. 搭建开发环境
3. 建立我们的 django 博客应用
4. 创建 django 博客的数据库模型
5. 让 django 完成翻译——迁移数据库模型
6. django 博客首页视图
7. 真正的 django 博客首页视图
8. 在 django admin 后台发布我们的文章
首页展示的是所有文章的列表,当用户看到感兴趣的文章时,他点击文章的标题或者继续阅读的按钮,应该跳转到文章的详情页面来阅读文章的详细内容。本节我们来开发博客的详情页面,有了前面的基础,套路都是一样的了:首先把相关的 url 和视图函数绑定在一起,然后实现视图函数,编写响应的模板让视图函数渲染模板。
回顾一下我们首页视图的 url,在 blog/urls.py 文件里,我们写了:
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.index, name='index'),
]
首页视图匹配的 url 去掉域名后其实就是一个空的字符串。对文章详情视图而言,每篇文章对应着不同的 url。比如我们可以把文章详情页面对应的视图设计成这个样子:当用户访问 <网站域名>/post/1/
时,显示的是第一篇文章的内容,而当用户访问 <网站域名>/post/2/
时,显示的是第二篇文章的内容,这里数字代表了第几篇文章,也就是数据库中 Post 记录的 id。下面依照这个规则来绑定 url 和视图:
from django.conf.urls import url
from . import views
app_name = 'blog'
urlpatterns = [
url(r'^$', views.index, name='index'),
url(r'^post/(?P<pk>[0-9]+)/$', views.detail, name='detail'),
]
django 使用正则表达式来匹配对应的域名。这里 r'^post/[0-9]+/$'
整个正则表达式刚好匹配我们上面定义的 url 规则。这条正则表达式的含义是,以 post/
开头,后跟一个至少一位数的数字,并且以 /
符号结尾,如 /post/1/、 /post/255/ 等都是符合规则的,[0-9]+ 表示数字 0 到 9 重复至少一次的数字。此外我们这里 (?P<pk>[0-9]+)
表示命名捕获组,其作用是从用户访问的 url 里把括号内匹配的字符串捕获并作为关键字参数传给视图函数 detail。比如当用户访问 /post/255/ 时(注意 django 并不关心域名,而只关心去掉域名后的相对 url),被括起来的部分 (?P<pk>[0-9]+)
匹配 255,那么这个 255 会在调用视图函数 detail 时被传递进去,实际上视图函数的调用就是这个样子:detail(request, pk=255)
。我们这里必须从 url 里捕获文章的 id,因为只有这样我们才能知道用户访问的是那篇文章。
此外我们通过 app_name='blog'
告诉 django 这个 urls.py 模块是属于 blog 应用的,其具体作用会在下面介绍。
为了方便地生成上述的 url,我们在 Post 模型里定义一个 get_absolute_url
方法,注意 Post 本身是一个 Python 类,在类中我们是可以定义任何方法的。
blog/models.py
from django.db import models
from django.utils.six import python_2_unicode_compatible
# 导入 reverse 函数
from django.urls import reverse
@python_2_unicode_compatible
class Category(models.Model):
...
@python_2_unicode_compatible
class Tag(models.Model):
...
@python_2_unicode_compatible
class Post(models.Model):
...
def __str__(self):
return self.title
# 自定义 get_absolute_url 方法
def get_absolute_url(self):
return reverse('blog:detail', kwargs={'pk': self.pk})
注意到 URL 配置中 url(r'^post/(?P<pk>[0-9]+)/$', views.detail, name='detail')
,我们设定、的 name='detail'
,这里派上了用场。看到这个 reverse 函数,它的第一个参数的值是 'blog:detail'
,意思是 blog 应用下的 name=detail 函数,由于我们在上面通过 app_name = 'blog'
告诉了 django 这个 URL 模块是属于 blog 应用的,因此 django 能够顺利地找到 blog 应用下 name 为 detail 的视图函数,于是 django 会去解析这个视图函数对应的 url,我们这里 detail 对应的规则就是 post/(?P<pk>[0-9]+)/
这个正则表达式规则,而正则表达式部分会被后面传入的参数 pk 替换,所以,如果 post 的 id 是 255 的话,那么 get_absolute_url 函数返回的就是 /post/255/ ,这样 Post 自己就生成了自己的 url。
接下来就是实现我们的 detail 视图函数了:
blog/views.py
from django.shortcuts import render, get_object_or_404
from .models import Post
def index(request):
# ...
def detail(request, pk):
post = get_object_or_404(Post, pk=pk)
return render(request, 'blog/detail.html', context={'post': post})
视图函数很简单,它根据我们从 url 捕获的文章 id(也就是 pk,pk 意为主键,这里的主键就是 id)获取我们的 post,然后传递给模板。注意这里我们用到了从 django.shortcuts 模块导入的 get_object_or_404 方法,其作用就是当传入的 pk 对应的 Post 在数据库存在时,就返回找到的 post,如果不存在,就给用户返回一个 404 错误,表明用户请求的文章不存在。
接下来就是书写模板文件,从文件夹中把 single.html 拷贝到 template/blog 的目录下(和 index.html 在同一级目录),然后改名为 detail.html。在 index 页面博客文章列表的标题和继续阅读按钮填上链接,让用户点击后可以跳转到 detail 页面:
templates/index.html
<article class="post post-1">
<header class="entry-header">
<h1 class="entry-title">
<a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
</h1>
...
</header>
<div class="entry-content clearfix">
...
<div class="read-more cl-effect-14">
<a href="{{ post.get_absolute_url }}" class="more-link">继续阅读 <span class="meta-nav">→</span></a>
</div>
</div>
</article>
{% empty %}
<div class="no-post">暂时还没有发布的文章!</div>
{% endfor %}
这里我们修改两个地方,第一个是文章标题处:
<h1 class="entry-title">
<a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
</h1>
我们把 a 标签的 href 的值改成了 {{ post.get_absolute_url }},回顾一下模板变量的用法,由于 get_absolute_url 这个方法(我们定义在 Post 类中的)返回的是 post 对应的 url,因此这里 {{ post.get_absolute_url }} 最终会被替换成该 post 自身的 url。
同样,第二处修改的是继续阅读按钮的链接:
<a href="{{ post.get_absolute_url }}" class="more-link">继续阅读 <span class="meta-nav">→</span>
</a>
这样当我们点击首页文章的标题或者继续阅读按钮后就会跳转到该篇文章对应的详情页面了。然而如果你尝试跳转到详情页后,你会发现样式是乱的。这在[写首页视图函数][]时讲过,由于我们直接复制的模板,还没有正确地处理静态文件。我们可以按照那一节的方法修改静态文件的引入路径,但是很麻烦。你会发现在任何页面都是需要引入这些静态文件的,如果每个页面都要修改会很麻烦,而且代码都是重复的。下面就介绍 django 模板继承的方法来帮我们消除这些重复操作。
我们看到 index.html 文件和 detail.html 文件除了 main 包裹的部分不同外,其他地方都是相同的,我们可以把相同的部分抽取出来,放到 base.html 里。首先在 templates 目录下新建一个 base.html 文件,把 index.html 的内容全部拷贝过来,然后删掉 main 标签包裹的内容,替换成如下的内容。
templates/base.html
...
<main class="col-md-8">
{% block main %}
{% endblock main %}
</main>
...
这里 {% block main %}{% endblock main %} 是一个占位框,下面我们会看到它的作用。、
在 index.html 里,我们使用 {% extends 'base.html' %} 继承 base.html,这样就把 base.html 里的代码继承了过来,另外在 {% block main %}{% endblock main %} 包裹的地方填上 index 页面应该显示的内容:
{% extends 'base.html' %}
{% block main %}
{% for post in post_list %}
<article class="post post-1">
...
</article>
{% empty %}
<div class="no-post">暂时还没有发布的文章!</div>
{% endfor %}
{% endblock main %}
这样 base.html 里的代码加上 {% block main %}{% endblock main %} 里的代码就和最开始 index.html 里的代码一样了。这就是模板继承的作用,公共部分的代码放在 base.html 里,不同部分的代码通过替换 {% block main %}{% endblock main %} 占位框里的内容即可。
detail 页面处理起来就简单了,同样继承 base.html ,在 {% block main %}{% endblock main %} 里填充 detail.html 页面应该显示的内容。
blog/detail.html
{% extends 'base.html' %}
{% block main %}
<article class="post post-1">
...
</article>
{% endblock main %}
修改 article 标签下的一些内容,让其显示文章的实际数据。
<header class="entry-header">
<h1 class="entry-title">{{ post.title }}</h1>
<div class="entry-meta">
<span class="post-category"><a href="#">{{ post.category.name }} </a></span>
<span class="post-date">
<a href="#">
<time class="entry-date" datetime="{{ post.created_time }}">{{ post.created_time }} </time>
</a>
</span>
<span class="post-author"><a href="#">{{ post.author }} </a></span>
<span class="comments-link"><a href="#">4 Comments</a></span>
</div>
</header>
<div class="entry-content clearfix">
{{ post.body }}
</div>
再次从首页点击一篇文章的标题跳转到详情页面,可以看到预期效果了!