通过创建博客学习Django-2
本篇文章主要对追梦人物的博客《使用 django 开发一个个人博客》进行总结
https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/65/
创作后台开启,请开始你的表演
创建 admin 后台管理员账户
上文已创建,如果没有创建,则运行
> pipenv run python manage.py createsuperuser
用户名 (leave blank to use 'yangxg'): admin
电子邮件地址: admin@example.com
Password: # 密码输入过程中不会有任何字符显示
Password (again):
Superuser created successfully.
在 admin 后台注册模型
blog/admin.py
from django.contrib import admin
from .models import Post, Category, Tag
admin.site.register(Post)
admin.site.register(Category)
admin.site.register(Tag)
访问 http://127.0.0.1:8000/admin/查看效果
定制 admin 后台
汉化 blog 应用
1.blog汉化
- 在 blog 应用下有一个 app.py 模块
from django.apps import AppConfig
class BlogConfig(AppConfig):
name = 'blog'
- 修改 app 在 admin 后台的显示名字,添加 verbose_name 属性
class BlogConfig(AppConfig):
name = 'blog'
verbose_name = '博客'
- 在 settings 中修改注册应用:
INSTALLED_APPS = [
'django.contrib.admin',
...
'blog.apps.BlogConfig', # 注册 blog 应用
]
2.Post 、Tag 和 Category 模型汉化:
class Post(models.Model):
...
author = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
verbose_name = '文章'
verbose_name_plural = verbose_name
def __str__(self):
return self.title
class Category(models.Model):
name = models.CharField(max_length=100)
class Meta:
verbose_name = '分类'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Tag(models.Model):
name = models.CharField(max_length=100)
class Meta:
verbose_name = '标签'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
- 注册的 model 显示为中文,配置 model 特性是通过 model 的内部类
Meta
中来定义 - 通过
verbose_name
来指定对应的 model 在 admin 后台的显示名称,verbose_name_plural
用来表示多篇文章时的复数显示形式 - 英语中,如果有多篇文章,就会显示为 Posts,表示复数,中文没有复数表现形式,所以定义为和
verbose_name
一样
3.post 的表单的 label汉化
label 由定义在 model 中的 Field 名转换而来,所以在 Field 中修改
class Post(models.Model):
title = models.CharField('标题', max_length=70)
body = models.TextField('正文')
created_time = models.DateTimeField('创建时间')
modified_time = models.DateTimeField('修改时间')
excerpt = models.CharField('摘要', max_length=200, blank=True)
category = models.ForeignKey(Category, verbose_name='分类', on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag, verbose_name='标签', blank=True)
author = models.ForeignKey(User, verbose_name='作者', on_delete=models.CASCADE)
- 位置参数值即为 field 应该显示的名字(如果不传,django 自动根据 field 名生成),参数的名字也叫
verbose_name
,绝大部分 field 这个参数都位于第一个位置, - 由于
ForeignKey
、ManyToManyField
第一个参数必须传入其关联的 Model,所以 category、tags 这些字段我们使用了关键字参数verbose_name
文章列表显示更加详细的信息
blog/admin.py
from django.contrib import admin
from .models import Post, Category, Tag
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'created_time', 'modified_time', 'category', 'author']
# 把新增的 Postadmin 也注册进来
admin.site.register(Post, PostAdmin)
admin.site.register(Category)
admin.site.register(Tag)
查看admin Post 列表页面效果
简化新增文章的表单
1.在blog/admin.py 中定义PostAdmin
配置 Post 在 admin 后台的一些展现形式
list_display
属性控制 Post 列表页展示的字段fields
属性控制表单展现的字段
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'created_time', 'modified_time', 'category', 'author']
fields = ['title', 'body', 'excerpt', 'category', 'tags']
2.填充文章作者
- save_model 方法:obj.save()。作用是将此 Modeladmin 关联注册的 model 实例(这里 Modeladmin 关联注册的是 Post)保存到数据库
- 这个方法接收四个参数,其中前两个,一个是 request,即此次的 HTTP 请求对象,第二个是 obj,即此次创建的关联对象的实例
通过复写此方法,就可以将 request.user 关联到创建的 Post 实例上,然后将 Post 数据再保存到数据库:
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'created_time', 'modified_time', 'category', 'author']
fields = ['title', 'body', 'excerpt', 'category', 'tags']
def save_model(self, request, obj, form, change):
obj.author = request.user
super().save_model(request, obj, form, change)
3.填充创建时间
Model 中定义的每个 Field 都接收一个 default 关键字参数,
- 参数的含义是:如果将 model 的实例保存到数据库时,对应的 Field 没有设置值,那么 django 会取这个 default 指定的默认值,将其保存到数据库
- 因此,对于文章创建时间这个字段,初始没有指定值时,默认应该指定为当前时间,所以刚好可以通过 default 关键字参数指定:
from django.utils import timezone
class Post(models.Model):
...
created_time = models.DateTimeField('创建时间', default=timezone.now)
...
- default 既可以指定为一个常量值,也可以指定为一个可调用(callable)对象,
- timezone.now 函数返回当前时间
4.填充修改时间
因为每次保存模型时,都应该修改 modified_time 的值
通过复写 save_model
方法,在 model 被 save 到数据库前指定 modified_time
的值为当前时间:
from django.utils import timezone
class Post(models.Model):
...
def save(self, *args, **kwargs):
self.modified_time = timezone.now()
super().save(*args, **kwargs)
- 调用父类的 save 以执行数据保存回数据库
https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/66/
开发博客文章详情页
设计文章详情页的 URL
当用户访问 <网站域名>/posts/1/ 时,显示的是第一篇文章的内容:
blog/urls.py
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path('', views.index, name='index'),
path('posts/<int:pk>/', views.detail, name='detail'),
]
'posts/<int:pk>/'
匹配 URL 规则,含义是以 posts/ 开头,后跟一个整数,并且以 / 符号结尾,如 posts/1/、 posts/255/ 等都是符合规则的<int:pk>
从用户访问的 URL 里把匹配到的数字捕获并作为关键字参数传给其对应的视图函数detail
区分视图函数
- 视图函数命名空间:通过
app_name='blog'
告诉 django 这个 urls.py 模块是属于 blog 应用 - 通过
app_name
来指定命名空间
为了方便地生成上述的 URL,我们在 Post
类里定义一个 get_absolute_url
方法,注意 Post
本身是一个 Python 类,在类中我们是可以定义任何方法的
blog/models.py
from django.contrib.auth.models import User
from django.db import models
from django.urls import reverse
from django.utils import timezone
class Post(models.Model):
...
def __str__(self):
return self.title
# 自定义 get_absolute_url 方法
# 记得从 django.urls 中导入 reverse 函数
def get_absolute_url(self):
return reverse('blog:detail', kwargs={'pk': self.pk})
- URL 配置中的
path('posts/<int:pk>/', views.detail, name='detail')
,设定的name='detail'
在这里派上了用场 reverse
函数,第一个参数的值是'blog:detail'
,意思是 blog 应用下的name=detail
的函数- detail 对应的规则是
posts/<int:pk>/
int 部分会被后面传入的参数pk
替换,所以,如果Post
的 id(或者 pk,这里 pk 和 id 是等价的) 是 255 的话,那么get_absolute_url
函数返回是 /posts/255/ ,这样 Post 自己就生成了自己的 URL
编写 detail 视图函数
log/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 是等价的)获取数据库中文章 id 为该值的记录,然后传递给模板
- 从 django.shortcuts 模块导入的
get_object_or_404
方法,作用是当传入的 pk 对应的 Post 在数据库存在时,就返回对应的post
,如果不存在,就给用户返回一个 404 错误,表明用户请求的文章不存在
编写详情页模板
从下载的博客模板把 single.html 拷贝到 templates\blog 目录下(和 index.html 在同一级目录),然后改名为 detail.html
blogproject\
manage.py
blogproject\
__init__.py
settings.py
...
blog/
__init__.py
models.py
,,,
templates\
blog\
index.html
detail.html
在 index 页面博客文章列表的标题和继续阅读按钮写上超链接跳转的链接,即文章 post
对应的详情页的 URL,让用户点击后可以跳转到 detail 页面:
templates/blog/index.html
<article class="post post-{{ post.pk }}">
<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 %}
{{ post.get_absolute_url }}
最终会被替换成该post
自身的 URL
模板继承
将 index.html 文件和 detail.html 文件相同的部分放到 base.html,在 templates 目录下新建一个 base.html 文件:
blogproject\
manage.py
blogproject\
__init__.py
settings.py
...
blog\
__init__.py
models.py
,,,
templates\
base.html
blog\
index.html
detail.html
把 index.html 的内容全部拷贝到 base.html 文件里,删掉 main 标签包裹的内容,替换成如下的内容:
templates/base.html
...
<main class="col-md-8">
{% block main %}
{% endblock main %}
</main>
<aside class="col-md-4">
{% block toc %}
{% endblock toc %}
...
</aside>
...
- block 模板标签,其作用是占位,
{% block main %}
{% endblock main %}
是一个占位框,main 是给 block 取的名字 - aside 标签下加了一个
{% block toc %}
{% endblock toc %}
占位框,因为 detail.html 中 aside 标签下会多一个目录栏 - 当
{% block toc %}
{% endblock toc %}
中没有任何内容时,{% block toc %}
{% endblock toc %}
在模板中不会显示;但当其中有内容是,模板就会显示 block 中的内容
在 index.html 里,我们在文件最顶部使用 {% extends 'base.html' %}
继承 base.html,在 {% block main %}
{% endblock main %}
包裹的地方填上 index 页面应该显示的内容:
templates/blog/index.html
{% extends 'base.html' %}
{% block main %}
{% for post in post_list %}
<article class="post post-1">
...
</article>
{% empty %}
<div class="no-post">暂时还没有发布的文章!</div>
{% endfor %}
<!-- 简单分页效果
<div class="pagination-simple">
<a href="#">上一页</a>
<span class="current">第 6 页 / 共 11 页</span>
<a href="#">下一页</a>
</div>
-->
<div class="pagination">
...
</div>
{% endblock main %}
- 模板继承的作用:公共部分的代码放在 base.html 里,而其它页面不同的部分通过替换
{% block main %}
{% endblock main %}
占位标签里的内容即可
detail 页面继承 base.html ,在 {% block main %}
{% endblock main %}
里填充 detail.html 页面应该显示的内容,以及在 {% block toc %}
{% endblock toc %}
中填写 base.html 中没有的目录部分的内容
templates/blog/detail.html
{% extends 'base.html' %}
{% block main %}
<article class="post post-1">
...
</article>
<section class="comment-area">
...
</section>
{% endblock main %}
{% block toc %}
<div class="widget widget-content">
<h3 class="widget-title">文章目录</h3>
<ul>
<li>
<a href="#">教程特点</a>
</li>
<li>
<a href="#">谁适合这个教程</a>
</li>
<li>
<a href="#">在线预览</a>
</li>
<li>
<a href="#">资源列表</a>
</li>
<li>
<a href="#">获取帮助</a>
</li>
</ul>
</div>
{% endblock toc %}
修改 article 标签下的一些内容,让其显示文章的实际数据:
<article class="post post-{{ post.pk }}">
<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 评论</a></span>
<span class="views-count"><a href="#">588 阅读</a></span>
</div>
</header>
<div class="entry-content clearfix">
{{ post.body }}
</div>
</article>
再次从首页点击一篇文章的标题或者继续阅读按钮跳转到详情页面,查看预期效果:
https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/67/
让博客支持 Markdown 语法和代码高亮
安装 Python Markdown
在项目根目录下运行命令 pipenv install markdown
在 detail 视图中解析 Markdown
所写的博客文章在 Post
的 body
属性里,回到详情页视图函数,对 post
的 body
的值做一下解析,把 Markdown 文本转为 HTML 文本再传递给模板:
blog/views.py
import markdown
from django.shortcuts import get_object_or_404, render
from .models import Post
def detail(request, pk):
post = get_object_or_404(Post, pk=pk)
post.body = markdown.markdown(post.body,
extensions=[
'markdown.extensions.extra',
'markdown.extensions.codehilite',
'markdown.extensions.toc',
])
return render(request, 'blog/detail.html', context={'post': post})
extensions
是对 Markdown 语法的拓展:
extra
包含很多基础拓展,codehilite
是语法高亮拓展,toc
允许自动生成目录
safe 标签
为解除转义,在模板变量后使用 safe
过滤器即可,告诉 django,这段文本是安全的
在模板中找到展示博客文章内容的 {{ post.body }}
部分,为其加上 safe 过滤器:{{ post.body|safe }}
代码高亮
代码高亮我们借助 js 插件来实现,其原理就是 js 解析整个 html 页面,然后找到代码块元素,为代码块中的元素添加样式。
- highlight.js 提供基础的代码高亮
- 和 highlightjs-line-numbers.js 为代码块添加行号。
在 base.html 的 head 标签里引入代码高亮的样式,样式文件通过 CDN 引入,同时在 style 标签里自定义了一点元素样式。(style 标签插入后可能出现异常)
<head>
...
<link href="https://cdn.bootcss.com/highlight.js/9.15.8/styles/github.min.css" rel="stylesheet">
<style>
.codehilite {
padding: 0;
}
/* for block of numbers */
.hljs-ln-numbers {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
text-align: center;
color: #ccc;
border-right: 1px solid #CCC;
vertical-align: top;
padding-right: 5px;
}
.hljs-ln-n {
width: 30px;
}
/* for block of code */
.hljs-ln .hljs-ln-code {
padding-left: 10px;
white-space: pre;
}
</style>
</head>
然后引入 js 文件,因为应该等整个页面加载完,插件再去解析代码块,把 js 文件的引入放在 body 底部:
<body>
<script src="https://cdn.bootcss.com/highlight.js/9.15.8/highlight.min.js"></script>
<script src="https://cdn.bootcss.com/highlightjs-line-numbers.js/2.7.0/highlightjs-line-numbers.min.js"></script>
<script>
hljs.initHighlightingOnLoad();
hljs.initLineNumbersOnLoad();
</script>
</body>
https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/68/
Markdown 文章自动生成目录,提升阅读体验
在文中插入目录
markdown.extensions.toc
是自动生成目录的拓展
在书写 Markdown 文本时,在想生成目录的地方插入 [ TOC ] 标记即可。例如新写一篇 Markdown 博文,其 Markdown 文本内容如下:
[TOC]
## 我是标题一
这是标题一下的正文
## 我是标题二
这是标题二下的正文
### 我是标题二下的子标题
这是标题二下的子标题的正文
## 我是标题三
这是标题三下的正文
在页面的任何地方插入目录
在侧边栏插入目录
blog/views.py
def detail(request, pk):
post = get_object_or_404(Post, pk=pk)
md = markdown.Markdown(extensions=[
'markdown.extensions.extra',
'markdown.extensions.codehilite',
'markdown.extensions.toc',
])
post.body = md.convert(post.body)
post.toc = md.toc
return render(request, 'blog/detail.html', context={'post': post})
- 先实例化一个
markdown.Markdown
对象md
,传入extensions
参数 - 使用该实例的
convert
方法将post.body
中的 Markdown 文本解析成 HTML 文本 - 实例
md
多出一个toc
属性,这个属性的值就是内容的目录,我们把md.toc
的值赋给post.toc
属性(要注意这个 post 实例本身是没有 toc 属性的,我们给它动态添加了 toc 属性)
在博客文章详情页的文章目录侧边栏渲染文章的目录,删掉占位用的目录内容,替换成如下代码:
{% block toc %}
<div class="widget widget-content">
<h3 class="widget-title">文章目录</h3>
{{ post.toc|safe }}
</div>
{% endblock toc %}
- 用模板变量标签 {{ post.toc }} 显示模板变量的值,注意 post.toc 实际是一段 HTML 代码,我们知道 django 会对模板中的 HTML 代码进行转义,所以要使用 safe 标签防止 django 对其转义
处理空目录
- 只有在文章存在目录结构时,才显示侧边栏的目录
- 分析 toc 的内容,如果有目录结构,ul 标签中就有值,否则就没有值
- 使用正则表达式来测试 ul 标签中是否包裹有元素来确定是否存在目录
(注意:可能需要调用re模块)
def detail(request, pk):
post = get_object_or_404(Post, pk=pk)
md = markdown.Markdown(extensions=[
'markdown.extensions.extra',
'markdown.extensions.codehilite',
'markdown.extensions.toc',
])
post.body = md.convert(post.body)
m = re.search(r'<div class="toc">\s*<ul>(.*)</ul>\s*</div>', md.toc, re.S)
post.toc = m.group(1) if m is not None else ''
return render(request, 'blog/detail.html', context={'post': post})
正则表达式去匹配生成的目录中包裹在 ul
标签中的内容:
- 如果不为空,说明目录把
ul
标签中的值提取出来(目的是只要包含目录内容的最核心部分,多余的 HTML 标签结构丢掉)赋值给post.toc
; - 否则,将 post 的 toc 置为空字符串,然后我们就可以在模板中通过判断
post.toc
是否为空,来决定是否显示侧栏目录
{% block toc %}
{% if post.toc %}
<div class="widget widget-content">
<h3 class="widget-title">文章目录</h3>
<div class="toc">
<ul>
{{ post.toc|safe }}
</ul>
</div>
</div>
{% endif %}
{% endblock toc %}
模板标签 {% if %}
表示条件判断,和 Python 中的 if 条件判断是类似的
美化标题的锚点 URL
#_1
是锚点,Markdown 在设置锚点时利用的是标题的值,由于通常我们的标题都是中文,需要修改一下传给 extentions
的参数:
blog/views.py
from django.utils.text import slugify
from markdown.extensions.toc import TocExtension
def detail(request, pk):
post = get_object_or_404(Post, pk=pk)
md = markdown.Markdown(extensions=[
'markdown.extensions.extra',
'markdown.extensions.codehilite',
# 记得在顶部引入 TocExtension 和 slugify
TocExtension(slugify=slugify),
])
post.body = md.convert(post.body)
m = re.search(r'<div class="toc">\s*<ul>(.*)</ul>\s*</div>', md.toc, re.S)
post.toc = m.group(1) if m is not None else ''
return render(request, 'blog/detail.html', context={'post': post})
extensions
中的toc
拓展不再是字符串markdown.extensions.toc
,而是TocExtension
的实例TocExtension
在实例化时其slugify
参数可以接受一个函数,这个函数将被用于处理标题的锚点值- 使用
django.utils.text
中的slugify
方法处理中文标题
效果如下:
https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/69/
自动生成文章摘要
覆写 save 方法
body
字段存储的是正文,excerpt
字段用于存储摘要。通过覆写模型的 save
方法,在数据被保存到数据库前,先从 body
字段摘取 N 个字符保存到 excerpt
字段中,从而实现自动摘要的目的
blog/models.py
import markdown
from django.utils.html import strip_tags
class Post(models.Model):
# 其它字段...
body = models.TextField()
excerpt = models.CharField(max_length=200, blank=True)
# 其它方法...
def save(self, *args, **kwargs):
self.modified_time = timezone.now()
# 首先实例化一个 Markdown 类,用于渲染 body 的文本。
# 由于摘要并不需要生成文章目录,所以去掉了目录拓展。
md = markdown.Markdown(extensions=[
'markdown.extensions.extra',
'markdown.extensions.codehilite',
])
# 先将 Markdown 文本渲染成 HTML 文本
# strip_tags 去掉 HTML 文本的全部 HTML 标签
# 从文本摘取前 54 个字符赋给 excerpt
self.excerpt = strip_tags(md.convert(self.body))[:54]
super().save(*args, **kwargs)
摘要方案:
- 先将
body
中的 Markdown 文本转为 HTML 文本,去掉 HTML 文本里的 HTML 标签,然后摘取文本的前 54 个字符作为摘要 - 然后在模板中适当的地方使用模板标签引用
{{ post.excerpt }}
显示摘要的值
templates/blog/index.html
<article class="post post-{{ post.pk }}">
...
<div class="entry-content clearfix">
<p>{{ post.excerpt }}...</p>
<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>
新添加一篇文章,才能触发 save 方法,此前添加的文章不会自动生成摘要,要手动保存一下触发 save 方法