通过创建博客学习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/查看效果

django 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 这个参数都位于第一个位置,
  • 由于 ForeignKeyManyToManyField 第一个参数必须传入其关联的 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 列表页面效果

img

简化新增文章的表单

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>

再次从首页点击一篇文章的标题或者继续阅读按钮跳转到详情页面,查看预期效果:

img


https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/67/

让博客支持 Markdown 语法和代码高亮

安装 Python Markdown

项目根目录下运行命令 pipenv install markdown

在 detail 视图中解析 Markdown

所写的博客文章在 Postbody 属性里,回到详情页视图函数,对 postbody 的值做一下解析,把 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 方法

posted @ 2020-07-15 19:23  ChungZhao  阅读(191)  评论(0编辑  收藏  举报