2、Django基础二

  • python: 3.6.5

  • django: 2.1.8

1. 项目准备

1.1 配置虚拟环境

pip3 install pipenv
mkdir test && cd test/
pipenv --python 3.6.5
pipenv shell

1.2 安装Django

((test) ) [root@k8s test]# pipenv install django==2.1.8

1.3 创建项目

((test) ) [root@k8s test]# django-admin startproject project

1.4 修改时区

''' settings.py
ALLOWED_HOSTS = ["*"]

LANGUAGE_CODE = 'zh-hans'

TIME_ZONE = 'Asia/Shanghai'
'''

1.5 创建超级管理员

((test) ) [root@k8s project]# python manage.py makemigrations
No changes detected
((test) ) [root@k8s project]# python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying sessions.0001_initial... OK

((test) ) [root@k8s project]# python manage.py createsuperuser
用户名 (leave blank to use 'root'): admin
电子邮件地址: admin@qq.com
Password: 
Password (again): 
密码跟 电子邮件地址 太相似了。
密码长度太短。密码必须包含至少 8 个字符。
这个密码太常见了。
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.

1.6 创建APP应用及注册

((test) ) [root@k8s project]# python manage.py startapp blog
''' settings.py 文件
INSTALLED_APPS = [
    ''''''
    'blog',
]
'''

2. 项目

2.1 数据模型分析

  • 分类: 一个分类下有多个文章

  • 标签: 一个文章下有多个标签,一个标签下有多个文章

  • 文章: 标题、摘要、内容、创建时间、修改时间、归属分类、归属标签、作者

2.2 数据模型创建

  • blog/models.py

from django.db import models

class Categorys(models.Model):
    ''' 分类表 '''
    name = models.CharField(max_length=100,verbose_name='分类')

    def __str__(self):
        return self.name


class Tags(models.Model):
    ''' 标签表 '''
    name = models.CharField(max_length=100, verbose_name='标签')

    def __str__(self):
        return self.name


class Posts(models.Model):
    ''' 文章表 '''
    title = models.CharField(max_length=100,verbose_name='标题')
    body = models.TextField(verbose_name='内容')
    ct_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
    mf_time = models.DateTimeField(auto_now=True, verbose_name='修改时间')
    excerpt = models.CharField(max_length=200, blank=True, null=True,verbose_name='内容摘要')
    looks = models.PositiveIntegerField(default=0,verbose_name='阅读量')
    categorys = models.ForeignKey(Categorys, on_delete=False,verbose_name='归属分类')
    tags = models.ManyToManyField(Tags, blank=True, null=True,verbose_name='归属标签')
    # author = models.ForeignKey(User)                         # 作者

    def __str__(self):
        return self.title
    
    # 自定义自增阅读量方法
    def increase_looks(self):
        self.looks += 1
        self.save(update_fields=['looks'])

2.3 数据库迁移

# 生成迁移脚本、将迁移信息写入数据库
((test) ) [root@k8s project]# python manage.py makemigrations
System check identified some issues:

WARNINGS:
blog.Posts.tags: (fields.W340) null has no effect on ManyToManyField.
Migrations for 'blog':
  blog/migrations/0001_initial.py
    - Create model Categorys
    - Create model Posts
    - Create model Tags
    - Add field tags to posts

((test) ) [root@k8s project]# python manage.py  migrate
System check identified some issues:

WARNINGS:
blog.Posts.tags: (fields.W340) null has no effect on ManyToManyField.
Operations to perform:
  Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
  Applying blog.0001_initial... OK

2.4 Admin后台注册模型

  • blog/admin.py

from django.contrib import admin
from blog.models import Category,Tag,Post

class PostAdmin(admin.ModelAdmin):
    list_display = ['title','excerpt', 'ct_time', 'mf_time', 'category']
    list_display_links = ['title']
    search_fields=['title']        # 以title搜索
    list_filter = ['category']     # 过滤
    list_per_page = 20             # 分页
    # list_editable = ['category'] # 列表编辑


admin.site.register(Category)
admin.site.register(Tag)
admin.site.register(Post,PostAdmin)

3. Django 博客首页 和 详情页

3.1 URL分析

  • 首页: 域名根路径

  • 文章: 每个文章对应不同的URL

    • 当用户访问 <网站域名>/post/1/ 时,显示的是第一篇文章的内容,

    • 当用户访问 <网站域名>/post/2/ 时,显示的是第二篇文章的内容。这里数字代表了第几篇文章,

3.2 创建 urls.py 文件

  • blog/urls.py

from django.urls import re_path
from blog import views

urlpatterns = [
    # 首页
    re_path(r'^$', views.index, name='index'),

    # ^: 以什么开头, $是以什么结尾。 [0-9]指单个数字, +代表前面的字符出现1次或者多次。
    # (?P<pk>[0-9]+) 关键字匹配
    # /post/1/   ====> 1满足正则规则的, 将pk=1
    re_path(r'^post/(?P<id>[0-9]+)/$', views.detail, name='detail'),
]

3.3 首页||文章详情视图

  • blog/views.py

from django.shortcuts import get_object_or_404, render
from blog.models import Post


def index(request):
    posts = Post.objects.all()
    return render(request, 'blog/index.html', context={'posts':posts})

def detail(request,id):
    '''
      get_object_or_404 方法,
      其作用就是当传入的 id 对应的 Post 在数据库存在时,就返回对应的 post ,
      如果不存在,就给用户返回一个 404 错误,表明用户请求的文章不存在。
    '''
    post = get_object_or_404(Post, id=id)
    post.increase_looks() # 执行阅读量自增方法
    return render(request, 'blog/detail.html', context={'post': post})

3.4 Model中自定义ID方法

  • blog/models.py

class Post(models.Model):
    ...
    def __str__(self):
        return self.title
    # 自定义 get_absolute_url 方法
    # 记得从 django.urls 中导入 reverse 函数
    # reverse相当于Flask里面的url_for, 根据视图函数名称反向获取对应的路由地址.
    # /post/1/
    def get_absolute_url(self):
        return reverse('detail', kwargs={'id': self.id})

3.5 项目 urls.py 添加 APP 路径映射

  • project/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path(r'', include('blog.urls')),
]

3.6 html 文件

# templates/blog/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>欢迎来到博客首页</h1>
</body>
</html>
# templates/blog/detail.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
{{ post.title }}
{{ post.created_time }}
{{ post.modified_time }}
{{ post.body }}

</body>
</html>

3.5 设置静态文件static和模板templates文件

  • 这里 static 与 项目同级, 存放静态文件

  • 这里 templates 与 项目同级, 存放 html 文件

# project/settings.py
TEMPLATES = [
    {
        ...
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        ...
    },
]


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/

STATIC_URL = '/static/'

# 静态文件存放路径
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, "static"),
]

4. Admin后台集成 markdown 格式编写

  • 参考: https://pypi.org/project/django-mdeditor/

4.1 安装django-mdeditor

# 使用别的版本上传图片可能会报错,参考官网
pipenv install django-mdeditor==0.1.18

4.2 注册 mdeditor APP

  • project/settings.py

INSTALLED_APPS = [
    ...
    'mdeditor',
]


# 当上传图片的时候会自动在项目下创建  uploads/editor 目录, editor 是下面 MDEDITOR_CONFIGS 定义
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'



# 下面可有可无,如果自定义话,修改下面文件内容
MDEDITOR_CONFIGS = {
    'default':{
        'width': '90% ',  # 自定义编辑框宽度
        'heigth': 500,  # 自定义编辑框高度
        'toolbar': ["undo", "redo", "|",
                    "bold", "del", "italic", "quote", "ucwords", "uppercase", "lowercase", "|",
                    "h1", "h2", "h3","h4", "h5", "h6", "|",
                    "list-ul", "list-ol", "hr", "|",
                    "link", "reference-link", "image", "code", "preformatted-text", "code-block", "table", "datetime"
                    "emoji", "html-entities", "pagebreak", "goto-line", "|",
                    "help", "info",
                    "||", "preview", "watch", "fullscreen"],  # 自定义编辑框工具栏
        'upload_image_formats': ["jpg", "jpeg", "gif", "png", "bmp", "webp"],  # 图片上传格式类型
        'image_folder': 'editor',  # 图片保存文件夹名称
        'theme': 'default',  # 编辑框主题 ,dark / default
        'preview_theme': 'default',  # 预览区域主题, dark / default
        'editor_theme': 'default',  # edit区域主题,pastel-on-dark / default
        'toolbar_autofixed': True,  # 工具栏是否吸顶
        'search_replace': True,  # 是否开启查找替换
        'emoji': True,  # 是否开启表情功能
        'tex': True,  # 是否开启 tex 图表功能
        'flow_chart': True,  # 是否开启流程图功能
        'sequence': True, # 是否开启序列图功能
        'watch': True,  # 是否开启实时预览
        'lineWrapping': False,  # 是否换行
        'lineNumbers': False  # 是否显示行号
    }
    
}

4.3 添加 mdeditor URL

  • project/urls.py

from django.conf.urls import url, include
from django.conf.urls.static import static
from django.conf import settings
...

urlpatterns = [
    ...
    url(r'mdeditor/', include('mdeditor.urls'))        # 添加路径
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

4.4 修改数据库字段类型

  • blog/models.py

from django.db import models
from mdeditor.fields import MDTextField

class Post(models.Model):
    ''' 文章表 '''
    body = MDTextField(verbose_name='内容')

4.5 生成数据文件迁移到数据库

python manage.py makemigrations 
python manage.py migrate

4.6 测试是否正常

4.7 django-mdeditor 错误

我们启动项目,进入文章发布页面。提示出错:

render() got an unexpected keyword argument 'renderer'

解决办法: 注释这一行

5. 渲染代码高亮显示,自动生成侧边目录

5.1 安装插件

# markdown 代码渲染插件, pygments 高亮插件
pipenv install markdown pygments

5.2 修改原 view.detail 视图

from django.shortcuts import get_object_or_404, render
from django.utils.text import slugify
from markdown.extensions.toc import TocExtension
from blog.models import Post
import markdown
import re

'''其他略...'''

def detail(request,id):
    post = get_object_or_404(Post, id=id)
    post.increase_looks() # 执行阅读量自增方法
    md = markdown.Markdown(extensions=[
        'markdown.extensions.extra',
        'markdown.extensions.codehilite',
        # 'markdown.extensions.toc',    # 使用这个方法显示的数字描边,无法显示中文: http://xxxx/post/3/#_14
        TocExtension(slugify=slugify),  # TocExtension 在实例化时其 slugify 参数可以接受一个函数,这个函数将被用于处理标题的锚点值
    ])

    post.body = md.convert(post.body)

    # 匹配生成的目录中包裹在 ul 标签中的内容,如果不为空,说明目录,就把 ul 标签中的值提取出来赋值给 post.toc;否则,将 post 的 toc 置为空字符串
    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})

5.3 修改详情页: detail.html

代码高亮: https://highlightjs.org/

Github: https://github.com/highlightjs/highlight.js/tree/main/src/styles

网络 CSS 文件: https://unpkg.com/browse/@highlightjs/cdn-assets@11.3.1/styles/

    <link rel="stylesheet" href="//unpkg.com/@highlightjs/cdn-assets@11.3.1/styles/github.min.css">
    <script src="//unpkg.com/@highlightjs/cdn-assets@11.3.1/highlight.min.js"></script>
    <script>hljs.highlightAll();</script>

    <!- safe 表示是安全的,不用转义 ->
    { post.body |safe }}


    {% block toc %}
    <!-- 在模板中通过判断 post.toc 是否为空,来决定是否显示侧栏目录 -->
        {% block toc %}
        {% if post.toc %}
        <div class="widget widget-content"
            style="width: 20%; height: 100%; background-color: aliceblue; position: fixed; overflow: auto; right: 0; top:0;">
            <h3 class="widget-title">文章目录</h3>
            <div class="toc">
                <ul>
                    {{ post.toc|safe }}
                </ul>
            </div>
        </div>
        {% endif %}
        {% endblock toc %}

5.4 测试效果

# 使用 `markdown.extensions.toc`
http://127.0.0.1:8000/posts/8/#_1
http://127.0.0.1:8000/posts/8/#_3


# 使用 TocExtension(slugify=slugify)
http://127.0.0.1:8000/posts/8/#我是标题一
http://127.0.0.1:8000/posts/8/#我是标题二下的子标题

5.5 界面太丑,使用开源摸版(百度一下)

例如: (自行删除,和继承)

6. 定制摸版标签

6.1 定制摸版标签

我的APP名是blog, 那我这里创建的 blog_template_tags.py ,存放摸版的标签 (如果APP多并且需要多个摸版标签的,可以单独创建一个 python 的文件包,所有的摸版标签放在单独的文件内)

  • blog/blog_template_tags.py

from django import template
from blog.models import Tag, Category,Post
from django.db.models.aggregates import Count


# 创建模板库对象
register = template.Library()

# 所有分类摸版标签, 并统计各个分类下的文章数
@register.simple_tag
def get_categories():
    return Category.objects.annotate(num_posts=Count('post'))
    # return Category.objects.all()
    # return Category.objects.annotate(num_posts=Count('post')).filter(num_posts__gt=0)
    

# 所有标签的摸版标签
@register.simple_tag
def get_tags():
    return Tag.objects.all()


#  所有文章摸版标签
@register.simple_tag
def get_posts():
    return Post.objects.all()

6.2 注册摸版标签

  • project/settings.py

TEMPLATES = [
    {
    ''' 略 '''
            'libraries': {
                'blog_template_tags': 'blog.blog_template_tags',  # 摸版标签路径
            }
        },
    },
]

6.3 使用摸版标签

  • templates/blog/base.html

{% load static %}

{% load blog_template_tags %}


                        <!-- 分类 -->
                        {% get_categories as categories %}
                        {% get_posts as posts %}
                        {% for categorie in categories %}
                        <li class="nav-item ">
                            <a data-toggle="collapse" href="#{{ categorie.id }}">
                                <i class="fas fa-layer-group"></i>
                                <p>{{ categorie.name }}({{ categorie.num_posts }})</p>
                                <span class="caret"></span>
                            </a>
                            <div class="collapse" id="{{ categorie.id }}">
                                <ul class="nav nav-collapse">
                                    {% for post in posts %}
                                    {% if post.category_id == categorie.id %}
                                    <li>
                                        <a href="#">
                                            <span class="sub-item">{{ post.title }}</span>
                                        </a>
                                    </li>
                                    {% endif %}
                                    {% endfor %}
                                </ul>
                            </div>
                        </li>
                        {% endfor %}
                        <!-- 分类结束 -->

6.4 测试效果

Last updated