9. 博客文章详情页

本教程内容已过时,更新版教程请访问: 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>

再次从首页点击一篇文章的标题跳转到详情页面,可以看到预期效果了!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,999评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,102评论 1 302
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,709评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,439评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,846评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,881评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,062评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,783评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,517评论 1 248
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,762评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,241评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,236评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,145评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,941评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,965评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,802评论 2 275

推荐阅读更多精彩内容