Django 博客demo

  1. Bootstrap5
    1. 简单介绍
    2. 引入Bootstrap5
      1. 下载相关资源
      2. 加载静态资源
      3. 在模版中引入Bootstrap5
    3. 导航栏Header
      1. 复制导航栏的源码
      2. 修改导航栏的源码
        1. 修改logo
        2. 修改颜色
        3. 添加边框
        4. 总的效果
    4. 首页布局
      1. 调整背景
      2. container
        1. 主要特点
        2. 三种容器类型
        3. 使用
      3. 增加边距
        1. Margin(外边距)
        2. Padding(内边距)
      4. 设置圆角
      5. 设置卡片
    5. 详情页布局
      1. 初步效果
      2. 作者与发布时间
      3. 博客详情
      4. 发表评论
      5. 评论列表
    6. 发布博客页面布局
      1. 代码
      2. 如何引入富文本编辑器
    7. 登录注册页面的布局
      1. 登陆
      2. 注册
    8. Django 模版化
      1. 父模版base.html
      2. 子模板pub_blog.html
  • 注册功能
    1. 发送邮箱验证码
      1. 开启QQ邮箱服务
      2. 配置参数
      3. 编写发送邮箱的后端代码
    2. 存储验证码
      1. 生成存储验证码的表
        1. 注册APP
        2. 创建Model
        3. 运行迁移脚本创建表
        4. 我使用SQLite作数据库的原因
      2. 修改代码
    3. 获取验证码功能
      1. 引入JQuery
      2. 给按钮定义ID
      3. 编写JQuery代码
      4. 引入自己编写的JS代码
    4. 实现注册功能
      1. 创建表单模型
      2. 解释User = get_user_model()
        1. get_user_model() 返回的是什么
        2. 为什么不直接引用
        3. get_user_model() 的作用
        4. 为什么不能提前赋值?
        5. 如何auth的User不满足业务需求怎么办
        6. auth模块的优势
      3. 后端代码
      4. 添加csrf的token
  • 实现登陆功能
    1. 登陆表单的Form
    2. 添加csrf的token
    3. 编写后端验证逻辑
    4. login(request, user) 干了哪些事情
    5. 登录和注册按钮的跳转
    6. 登录和非登录状态的切换
      1. user.is_authenticated
        1. 是什么
        2. 为什么能在模板中直接使用 user.is_authenticated?
    7. 退出登录功能
      1. 后端代码
      2. 前端代码
      3. logout干了些什么
  • 实现博客发布功能
    1. 发布博客访问限制
      1. 前端引入跳转的URL
      2. 后端校验是否登录
      3. @login_required 检查用户登录状态的步骤
    2. 创建相关模型
    3. Django的Admin系统的使用
      1. 登录
    4. 创建superuser
      1. 添加blog的相关配置
        1. 添加admin配置
        2. 把配置项的名字改成中文
        3. 把配置属性变成中文
    5. 定义__str__(self)
      1. 在 Django Admin 中的表现
        1. 后台列表页(List View)
        2. 下拉选择框(ForeignKey/ManyToManyField)
      2. 其他 Django 场景
        1. Shell或日志调试
        2. 模板渲染
      3. 为什么不用 verbose_name?
    6. 发布博客后端代码
    7. 发布博客的前端内容
      1. HTML的内容
        1. 分类下拉框
        2. 引入jquery
        3. 给按钮绑定Id
        4. 补充csrf_token
      2. Js绑定点击事件
  • 展示博客详情
    1. 后端
    2. 前端
  • 评论功能
    1. 后端
    2. 前端
  • 代码高亮Highlight.js
  • 首页功能
    1. 后端代码
    2. 后端模版
  • 查询功能
    1. 后端代码
    2. 前端代码
  • Bootstrap5

    简单介绍

    Bootstrap 5 是最新版本的 Bootstrap(截至2025年),它是一个免费、开源的 前端框架,用于快速构建响应式、移动优先的网站和 Web 应用。

    引入Bootstrap5

    下载相关资源

    文件名 作用 是否必须
    bootstrap.min.css Bootstrap 的压缩版 CSS 样式文件 ✅ 必须
    bootstrap.min.js Bootstrap 的压缩版 JavaScript 功能文件 ✅ 必须(如需交互组件)
    popper.min.js Popper.js(用于下拉菜单、弹出框等定位) ✅ 必须(Bootstrap 5 依赖它)
    1. 访问 Bootstrap 5 官方下载页

    2. 点击 “Compiled CSS and JS” 下载完整包(包含所有文件)。

    3. 解压后,在 dist/ 文件夹中找到:

      • css/bootstrap.min.css
      • js/bootstrap.min.js
    4. Bootstrap 5 默认不提供单独 popper.min.js,而是内置在 bootstrap.bundle.min.js

    5. 正确做法:直接使用 bootstrap.bundle.min.js,避免手动管理 Popper。

    6. 如需下载 popper.min.js可以访问Popper 官方源码页面: https://unpkg.com/@popperjs/core@2/dist/umd/

    7. 下载后,放入到static的包下

    加载静态资源

    配置STATICFILES_DIRS

    # Django 方式
    STATICFILES_DIRS = [
        # 这个 static 可以变成 ABC 只是一个名称
        BASE_DIR / "static"
    ]
    

    配置builtins,这样可以以后不使用{% load static %}加载静态资源

    TEMPLATES = [
        {
            "BACKEND": "django.template.backends.django.DjangoTemplates",
            "DIRS": [],
            "APP_DIRS": True,
            "OPTIONS": {
                "context_processors": [
                    "django.template.context_processors.debug",
                    "django.template.context_processors.request",
                    "django.contrib.auth.context_processors.auth",
                    "django.contrib.messages.context_processors.messages",
                ],
                # 加在这里
                "builtins": ["django.templatetags.static"],
            },
        },
    ]
    

    在模版中引入Bootstrap5

    <head>
        <meta charset="UTF-8">
        <title>LinNote<title/>
        {% comment %} 加载CSS样式 {% endcomment %}
        <link rel="stylesheet" href="% static 'bootstrap5/bootstrap.min.css' %}">
        {% comment %} 加载JS脚本 {% endcomment %}
        <script src="{% static 'bootstrap5/popper.min.js'%}"></script>
        <script src="{% static 'bootstrap5/bootstrap.min.js'%}"></script>
    </head>
    

    导航栏Header

    复制导航栏的源码

    找到官网导航栏的Example,选一个自己喜欢的。然后右键,然后检查,然后找到elements,找header标签,最后复制,就可以复制源代码。

    复制源码之后,放在project/templates/index.html里面,记得要配置如下:

    TEMPLATES = [
        {
            'BACKEND': 'django.template.backends.django.DjangoTemplates',
            'DIRS': [BASE_DIR / 'templates'] # 要检查这里是否配置
            ,
            'APP_DIRS': True,
            'OPTIONS': {
                'context_processors': [
                    'django.template.context_processors.debug',
                    'django.template.context_processors.request',
                    'django.contrib.auth.context_processors.auth',
                    'django.contrib.messages.context_processors.messages',
                ],
            },
        },
    ]
    

    否者会报django.template.exceptions.TemplateDoesNotExist: index.html

    修改导航栏的源码

    <a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
        <img src="{% static 'image/logo001.png' %}" height="40">
    </a>
    

    修改颜色

    可以根据官方文档修改颜色

    <header class="p-3 text-bg-light">
    
    </header>
    

    添加边框

    可以根据官方文档修改边框

    <header class="p-3 text-bg-light border-bottom">
    
    </header>
    

    总的效果

    <!-- border-bottom: 底部边框 -->
    <header class="p-3 text-bg-light border-bottom">
        <div class="container">
            <div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
                <a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
                    <!-- 设置logo -->
                    <img src="{% static 'image/logo003.jpg' %}" height="40">
                </a>
                <ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
                    <!-- secondary 灰色 -->
                    <li><a href="#" class="nav-link px-2 text-secondary">首页</a></li>
                    <li><a href="#" class="nav-link px-2 text-secondary">发布博客</a></li>
                </ul>
                <form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3" role="search"> <input type="search"
                        class="form-control" placeholder="搜索..." aria-label="Search">
                </form>
                <!-- primary:蓝色 -->
                <div class="text-end"> <button type="button" class="btn btn-outline-primary">登录</button> <button
                        type="button" class="btn btn-primary">注册</button> </div>
            </div>
        </div>
    </header>
    

    以下是效果图:

    首页布局

    调整背景

    static/css/base.css下面写入如下css样式

    body {
        /* rgba :r-红色,g-绿色,b-蓝色,a-透明度 */
        /* (0, 0, 0, 0.1) 前面3个0代表黑色,后面0.1的透明度,会把黑色变成灰色 */
        background-color: rgba(0, 0, 0, 0.1);
    }
    

    index.html里面引入css样式

    <!-- 加载CSS样式 -->
    <link rel="stylesheet" href="{% static 'bootstrap5/bootstrap.min.css' %}">
    <link rel="stylesheet" href="{% static 'css/base.css' %}">
    

    实现效果:

    container

    container 是 Bootstrap 5 中最基础的布局容器,用于控制页面内容的宽度、对齐和响应式间距。它的核心作用是创建水平居中的内容区域,并自动适应不同屏幕尺寸。

    主要特点

    特性 说明
    响应式宽度 根据屏幕尺寸自动调整最大宽度(如 PC 端 1140px,平板 720px)。
    水平居中 自动左右外边距 auto,使内容始终居中。
    内边距(Gutters) 默认两侧有 12px 内边距(可通过 g-* 工具类调整)。
    嵌套支持 允许内部再嵌套 containercontainer-fluid

    三种容器类型

    类名 行为 适用场景
    .container 固定宽度,响应式断点调整宽度(默认)。 大多数常规布局(如博客、后台)。
    .container-fluid 100% 宽度,始终撑满视口。 全屏轮播、仪表盘。
    .container-{breakpoint} 特定断点前 100% 宽度,达到断点后变为固定宽度(如 .container-md)。 需要混合响应的场景。

    使用

    <main class="container bg-white">
        <h1>博客列表</h1>
    </main>
    

    增加边距

    边距相关的文档在spacing里面

    Margin(外边距)

    控制元素边界外部的透明区域,决定与其他元素的距离。

    <!-- border-bottom: 底部边框 -->
    <!-- m - for classes that set margin -->
    <!-- b - for classes that set margin-bottom or padding-bottom -->
    <!-- 3 - (by default) for classes that set the margin or padding to $spacer -->
    <header class="p-3 text-bg-light border-bottom mb-3">
    
    </header>
    

    Padding(内边距)

    控制元素内容与边框之间的空白区域。

    <!-- p - for classes that set padding -->
    <main class="container bg-white p-3">
        <h1>博客列表</h1>
    </main>
    

    设置圆角

    Borders里面,可以设置圆角

    <!-- p - for classes that set padding -->
    <!-- rounded 添加圆角 -->
    <main class="container bg-white p-3 rounded">
        <h1>博客列表</h1>
    </main>
    

    设置卡片

    官网的参考配置如下:

    • card
    • borders
    • Flex:CSS 的 Flexbox(弹性盒子布局) 是一种一维布局模型,通过 弹性容器(flex container)弹性项目(flex item) 的配合,可以轻松实现灵活的对齐、排序和空间分配。它用简单的属性(如 justify-contentalign-items)即可控制子元素的水平或垂直排列方式,完美适配响应式设计需求。
    <main class="container bg-white p-3 rounded">
        <h1>博客列表</h1>
        <!-- class="row" 指定一行 -->
        <!-- row-cols-2: 一行两列 -->
        <!-- row-gap-4: 设置行间距,间距为4 -->
        <div class="row row-cols-2 row-gap-4">
            <!-- class="col":指定 列 -->
            <div class="col">
                <!-- text-center:文字居中,删除就不会文字居中了 -->
                <div class="card">
                    <div class="card-header">
                        <!-- 这里展示文章标题 -->
                        <a href="#">Django入门</a>
                    </div>
                    <!-- height: 100px:设置card-body的固定高度 -->
                    <div class="card-body" style="height: 100px;">
                        <!-- 原来的标题不用了 -->
                        <!-- <h5 class="card-title"></h5> -->
                        <p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
                    </div>
                    <!-- d-flex justify-content-between :设置d-flex -->
                    <div class="card-footer text-muted d-flex justify-content-between">
                        <div>
                            <img src="{% static 'image/avatar.jpg'%}" class="rounded-circle" width="30" height="30">
                            <!-- 用户名 -->
                            Lin
                        </div>
                        <div>
                            <!-- 先填一个假的发布时间 -->
                            发布时间:2025年12月12日
                        </div>
    
                    </div>
                </div>
            </div>
        </div>
    </main>
    

    详情页布局

    初步效果

    <!-- p - for classes that set padding -->
    <!-- rounded 添加圆角 -->
    <main class="container bg-white p-3 rounded">
        <h1>Django入门详情</h1>
        <!-- hr: 会显示一条横线 -->
        <hr />
    </main>
    

    作者与发布时间

    <!-- p - for classes that set padding -->
    <!-- rounded 添加圆角 -->
    <main class="container bg-white p-3 rounded">
      <h1>Django入门详情</h1>
      <!-- hr: 会显示一条横线 -->
      <hr />
      <!-- mt-2: spacing内容,t - for classes that set margin-top or padding-top -->
      <div class="mt-2">
          <img src="{% static 'image/avatar.jpg'%}" class="rounded-circle" width="30" height="30">
          <!-- s - (start) for classes that set margin-left or padding-left in LTR, 
    									margin-right or padding-right in RTL -->
          <span class="ms-2">Lin</span>
          <span class="ms-2">于</span>
          <span class="ms-2">2025年12月12日</span>发布
      </div>
      <hr />
    </main>
    

    博客详情

    <!-- y - for classes that set both *-top and *-bottom -->
    <!-- py-2: 上下内边距调整2 -->
    <div class="py-2">
        这是博客详情
    </div>
    <hr />
    

    发表评论

    <div class="mt-2">
        <h3>评论(10)</h3>
        <form action="">
            <div class="mt-2">
                <!-- form-control: bootstrap的表单样式 -->
                <input type="text" class="form-control" placeholder="请输入评论">
            </div>
            <!-- text-end 的本质:相当于 CSS 的 text-align: right,
                    会将容器内所有行内/行内块元素向右对齐。 -->
             <!-- 按钮的默认表现:Bootstrap 的 .btn 按钮本质是 display: inline-block(行内块元素),
                    因此会受父容器文本对齐方式影响。 -->
            <div class="text-end mt-2">
                <!-- btn btn-primary:bootstrap的按钮样式 -->
                <button type="button" class="btn btn-primary">评论</button>
            </div>
        </form>
    </div>
    

    涉及bootstrap5的内容:

    评论列表

    更多列表组件查看List group

    <div class="mt-2">
        <!-- list-group list-group-flush: bootstrap的列表样式 -->
        <ul class="list-group list-group-flush">
            <li class="list-group-item">
                <!-- class="user-info" 和 class="create-time" 是 HTML 元素的类名(CSS 类),它们本身不带任何功能,
                    但通常被用作前端样式或脚本的“钩子”,让你可以通过 CSS 或 JavaScript 对这些元素进行样式设置或操作控制。 -->
                <!--  text-secondary: 设置字体颜色 -->
                <div class="d-flex justify-content-between text-secondary">
                    <!-- class="user-info":不是 Bootstrap 的默认类,而是开发者自定义的 -->
                    <div class="user-info">
                        <img src="{% static 'image/avatar.jpg'%}" class="rounded-circle" width="30" height="30">
                        <span class="ms-2">Lin</span>
                    </div>
                    <!-- class="create-time":不是 Bootstrap 的默认类,而是开发者自定义的 -->
                    <!-- line-height: 40px 是 CSS 属性,用于控制文本行高 
                      该日期文本会在一个 40px 高的透明盒子中垂直居中-->
                    <div class="create-time" style="line-height: 40px;">2025年12月12日 12:12</div>
                </div>
                <!-- 这里的目的是为了对齐作者  -->
                <div style="padding-left: 30px;">
                    <div class="ms-2">这是一个评论内容</div>
                </div>
            </li>
        </ul>
    </div>
    

    发布博客页面布局

    代码

    <head>
        <meta charset="UTF-8">
        <title>LinNote</title>
        <!-- 加载CSS样式 -->
        <link rel="stylesheet" href="{% static 'bootstrap5/bootstrap.min.css' %}">
        <link rel="stylesheet" href="{% static 'css/base.css' %}">
        <!-- 加载JS脚本 -->
        <script src="{% static 'bootstrap5/popper.min.js'%}"></script>
        <script src="{% static 'bootstrap5/bootstrap.min.js'%}"></script>
        <!-- 加载wangeditor 的css 样式 和 js 脚本 -->
        <link rel="stylesheet" href="{% static 'wangeditor/style.css' %}">
        <script src="{% static 'wangeditor/index.js'%}"></script>
        <!-- 定义富文本编辑器的样式 -->
        <!-- 这里不用,因为已经下载了,放在wangeditor/style.css下面 -->
        <!-- <link href="https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css" rel="stylesheet" /> -->
        <style>
            /* 工具栏+编辑器 */
            #editor—wrapper {
                border: 1px solid #ccc;
                z-index: 100;
                /* 按需定义 */
            }
    
            /* 工具栏 */
            #toolbar-container {
                border-bottom: 1px solid #ccc;
            }
    
            /* 编辑器 */
            #editor-container {
                height: 500px;
            }
        </style>
        <!-- 引入wangeditor的 JS代码 -->
        <script src="{% static 'js/pub_blog.js'%}"></script>
    </head>
    
    <body>
        <!-- border-bottom: 底部边框 -->
        <!-- m - for classes that set margin -->
        <!-- b - for classes that set margin-bottom or padding-bottom -->
        <!-- 3 - (by default) for classes that set the margin or padding to $spacer -->
        <header class="p-3 text-bg-light border-bottom mb-3">
            <div class="container">
                <div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
                    <a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
                        <!-- 设置logo -->
                        <img src="{% static 'image/logo003.jpg' %}" height="40">
                    </a>
                    <ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
                        <!-- secondary 灰色 -->
                        <li><a href="#" class="nav-link px-2 text-secondary">首页</a></li>
                        <li><a href="#" class="nav-link px-2 text-secondary">发布博客</a></li>
                    </ul>
                    <form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3" role="search"> <input type="search"
                            class="form-control" placeholder="搜索..." aria-label="Search">
                    </form>
                    <!-- primary:蓝色 -->
                    <div class="text-end"> <button type="button" class="btn btn-outline-primary">登录</button> <button
                            type="button" class="btn btn-primary">注册</button> </div>
                </div>
            </div>
        </header>
        <!-- p - for classes that set padding -->
        <!-- rounded 添加圆角 -->
        <main class="container bg-white p-3 rounded">
            <h1>发布博客</h1>
            <div class="mt-3">
                <form action="">
                    <div class="mb-3">
                        <!-- 以下是bootstrap的样式 -->
                        <lable class="form-lable">标题</lable>
                        <input type="text" name="title" class="form-control">
                    </div>
                    <div class="mb-3">
                        <!-- 以下是bootstrap的样式 -->
                        <lable class="form-lable">分类</lable>
                        <select name="category" class="form-select">
                            <!-- 这里先写两个假的数据,有序写成真的 -->
                            <option value="1">Python</option>
                            <option value="2">前端</option>
                        </select>
                    </div>
                    <div class="mb-3">
                        <lable class="form-lable">内容</lable>
                        <!--使用wangeditor的富文本编辑器-->
                        <div id="editor—wrapper">
                            <div id="toolbar-container"><!-- 工具栏 --></div>
                            <div id="editor-container"><!-- 编辑器 --></div>
                        </div>
                    </div>
                    <div class="mb-3 text-end">
                        <button class="btn btn-primary">发布</button>
                    </div>
                </form>
            </div>
        </main>
    </body>
    

    如何引入富文本编辑器

    1. 进入wangeditor的官网

    2. 引入 CSS 定义样式。项目中直接下载了文件。

      <!-- 定义富文本编辑器的样式 -->
      <!-- 这里不用,因为已经下载了,放在wangeditor/style.css下面 -->
      <!-- <link href="https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css" rel="stylesheet" /> -->
      <style>
          /* 工具栏+编辑器 */
          #editor—wrapper {
              border: 1px solid #ccc;
              z-index: 100;
              /* 按需定义 */
          }
      
          /* 工具栏 */
          #toolbar-container {
              border-bottom: 1px solid #ccc;
          }
      
          /* 编辑器 */
          #editor-container {
              height: 500px;
          }
      </style>
      
    3. 定义 HTML 结构

      <!--使用wangeditor的富文本编辑器-->
      <div id="editor—wrapper">
          <div id="toolbar-container"><!-- 工具栏 --></div>
          <div id="editor-container"><!-- 编辑器 --></div>
      </div>
      
    4. 引入 JS 创建编辑器

      • 引入js的相关资源

        <script src="{% static 'wangeditor/index.js'%}"></script>
        <!--这个不需要,因为已经下载了-->
        <!--<script src="https://unpkg.com/@wangeditor/editor@latest/dist/index.js"></script>-->
        
      • 创建编辑器,放在单独的js文件里面

        // 当浏览器完全加载页面中的所有内容(包括 HTML、CSS、图片、脚本、iframe 等外部资源)后,window.onload 会触发绑定的函数
        window.onload = function () {
            const { createEditor, createToolbar } = window.wangEditor
        
            const editorConfig = {
                placeholder: 'Type here...',
                // 这里有一个监听事件,哟任何变化都会在这里打印
                onChange(editor) {
                    const html = editor.getHtml()
                    console.log('editor content', html)
                    // 也可以同步到 <textarea>
                }
            }
        
            const editor = createEditor({
                selector: '#editor-container',
                html: '<p><br></p>',
                config: editorConfig,
                mode: 'default', // or 'simple'
            })
        }
        
      • 由于把js脚本单独放在一个文件里面,所有还需要再次引入这个js文件

        <!-- 引入wangeditor的 JS代码 -->
        <script src="{% static 'js/pub_blog.js'%}"></script>
        

    登录注册页面的布局

    登陆

    <!-- p - for classes that set padding -->
    <!-- rounded 添加圆角 -->
    <main class="container bg-white p-3 rounded">
        <!--m-auto:使用 Bootstrap 的工具类,水平居中容器(通过自动左右外边距实现)。  -->
        <!--style="max-width: 330px":使用 Bootstrap 的工具类,水平居中容器(通过自动左右外边距实现)。 -->
        <div style="max-width: 330px;" class="m-auto">
            <h1>登录</h1>
            <div class="mb-3">
                <input type="email" name="email" class="form-control" placeholder="邮箱">
            </div>
            <div class="mb-3">
                <input type="password" name="password" class="form-control" placeholder="密码">
            </div>
            <!-- bootstrap的复选框 -->
            <div class="form-check">
                <input class="form-check-input" type="checkbox" value="" id="flexCheckDefault">
                <label class="form-check-label" for="flexCheckDefault">
                    请记住我
                </label>
            </div>
            <div class="mb-3">
                <!-- w = width(宽度)
                 100 = 100%(百分比)
                 组合效果:设置元素的宽度为父容器宽度的 100%(等同于 CSS 的 width: 100%;) -->
                <button class="btn btn-primary w-100">立即登录</button>
            </div>
        </div>
    </main>
    

    注册

    <!-- p - for classes that set padding -->
    <!-- rounded 添加圆角 -->
    <main class="container bg-white p-3 rounded">
      <div style="max-width: 330px;" class="m-auto">
          <h1>注册</h1>
          <form action="" method="POST">
              <div class="mb-3">
                  <input type="text" name="username" class="form-control" placeholder="用户名">
              </div>
              <div class="mb-3">
                  <input type="email" name="email" class="form-control" placeholder="邮箱">
              </div>
              <div class="mb-3">
                  <!-- bootstrap的样式 -->
                  <div class="input-group">
                      <input type="text" class="form-control" name="captcha" placeholder="验证码"
                          aria-label="Recipient's username" aria-describedby="button-addon2">
                      <button class="btn btn-outline-secondary" type="button" id="">获取验证码</button>
                  </div>
              </div>
              <div class="mb-3">
                  <input type="password" name="password" class="form-control" placeholder="密码">
              </div>
              <div class="mb-3">
                  <!-- w = width(宽度)
                       100 = 100%(百分比)
                       组合效果:设置元素的宽度为父容器宽度的 100%(等同于 CSS 的 width: 100%;) -->
                  <button class="btn btn-primary w-100">注册</button>
              </div>
          </form>
      </div>
    </main>
    

    Django 模版化

    父模版base.html

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        {% comment %} 不同页面会有不同标题 {% endcomment %}
        <title>{% block title %}{% endblock title %}-LinNote</title>
        <!-- 加载CSS样式 -->
        <link rel="stylesheet" href="{% static 'bootstrap5/bootstrap.min.css' %}">
        <link rel="stylesheet" href="{% static 'css/base.css' %}">
        <!-- 加载JS脚本 -->
        <script src="{% static 'bootstrap5/popper.min.js'%}"></script>
        <script src="{% static 'bootstrap5/bootstrap.min.js'%}"></script>
        {% comment %} 这个位置留给以后加一下CSS 和 js 代码 {% endcomment %}
        {% block head %}{% endblock head %}
    </head>
    
    <body>
        <!-- border-bottom: 底部边框 -->
        <!-- m - for classes that set margin -->
        <!-- b - for classes that set margin-bottom or padding-bottom -->
        <!-- 3 - (by default) for classes that set the margin or padding to $spacer -->
        <header class="p-3 text-bg-light border-bottom mb-3">
            <div class="container">
                <div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
                    <a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
                        <!-- 设置logo -->
                        <img src="{% static 'image/logo003.jpg' %}" height="40">
                    </a>
                    <ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
                        <!-- secondary 灰色 -->
                        <li><a href="#" class="nav-link px-2 text-secondary">首页</a></li>
                        <li><a href="#" class="nav-link px-2 text-secondary">发布博客</a></li>
                    </ul>
                    <form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3" role="search"> <input type="search"
                            class="form-control" placeholder="搜索..." aria-label="Search">
                    </form>
                    <!-- primary:蓝色 -->
                    <div class="text-end"> <button type="button" class="btn btn-outline-primary">登录</button> <button
                            type="button" class="btn btn-primary">注册</button> </div>
                </div>
            </div>
        </header>
        <!-- p - for classes that set padding -->
        <!-- rounded 添加圆角 -->
        <main class="container bg-white p-3 rounded">
            {% comment %} 每个页面的主体部分代码 {% endcomment %}
            {% block main %}{% endblock %}
        </main>
    </body>
    
    </html>
    

    子模板pub_blog.html

    {% extends "base.html" %}
    
    {% block title %}发布{% endblock %}
    
    {% block head %}
      <!-- 加载wangeditor 的css 样式 he js 脚本 -->
        <link rel="stylesheet" href="{% static 'wangeditor/style.css' %}">
        <script src="{% static 'wangeditor/index.js'%}"></script>
        <!-- 定义富文本编辑器的样式 -->
        <!-- 这里不用,因为已经下载了,放在wangeditor/style.css下面 -->
        <!-- <link href="https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css" rel="stylesheet" /> -->
        <style>
            /* 工具栏+编辑器 */
            #editor—wrapper {
                border: 1px solid #ccc;
                z-index: 100;
                /* 按需定义 */
            }
    
            /* 工具栏 */
            #toolbar-container {
                border-bottom: 1px solid #ccc;
            }
    
            /* 编辑器 */
            #editor-container {
                height: 500px;
            }
        </style>
        <!-- 引入wangeditor的 JS代码 -->
        <script src="{% static 'js/pub_blog.js'%}"></script>
    
    {% endblock %}
    
    {% block main %}
            <h1>发布博客</h1>
            <div class="mt-3">
                <form action="">
                    <div class="mb-3">
                        <!-- 以下是bootstrap的样式 -->
                        <lable class="form-lable">标题</lable>
                        <input type="text" name="title" class="form-control">
                    </div>
                    <div class="mb-3">
                        <!-- 以下是bootstrap的样式 -->
                        <lable class="form-lable">分类</lable>
                        <select name="category" class="form-select">
                            <!-- 这里先写两个假的数据,有序写成真的 -->
                            <option value="1">Python</option>
                            <option value="2">前端</option>
                        </select>
                    </div>
                    <div class="mb-3">
                        <lable class="form-lable">内容</lable>
                        <!--使用wangeditor的富文本编辑器-->
                        <div id="editor—wrapper">
                            <div id="toolbar-container"><!-- 工具栏 --></div>
                            <div id="editor-container"><!-- 编辑器 --></div>
                        </div>
                    </div>
                    <div class="mb-3 text-end">
                        <button class="btn btn-primary">发布</button>
                    </div>
                </form>
            </div>
    {% endblock %}
    

    注册功能

    发送邮箱验证码

    开启QQ邮箱服务

    登陆QQ邮箱,在设置下面,找到账号

    找到POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务,选择开启服务

    点击开启服务之后,QQ邮箱会要求绑定手机,按照他的要求做即可

    短信发送完毕之后,QQ邮箱会发送如下授权码,用这个授权码,Django就可以用QQ邮箱向其他人发送验证码了

    配置参数

    在项目的settings.py文件下配置以下参数

    # SMTP(Simple Mail Transfer Protocol,简单邮件传输协议)是互联网上用于发送电子邮件的核心通信协议。
    # 发送邮件:将电子邮件从发件人传输到收件人的邮件服务器(如 QQ 邮箱、Gmail 等)。
    # 不涉及接收邮件:接收邮件使用其他协议(如 POP3/IMAP)。
    
    # 指定邮件后端实现,smtp.EmailBackend 表示使用 SMTP 协议发送邮件。
    EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
    # 是否启用 TLS 加密(推荐 True,端口 587 时必启用)。
    EMAIL_USE_TLS = True
    # SMTP 服务器地址,这里是 QQ 邮箱的服务器。
    EMAIL_HOST = 'smtp.qq.com'
    # SMTP 服务器端口号,QQ 邮箱的 TLS 端口为 587(SSL 端口为 465)。
    EMAIL_PORT = 587
    # 发件人邮箱地址(需与 SMTP 服务器匹配)。
    EMAIL_HOST_USER = 'xxxxxx@qq.com'
    # 授权码(非邮箱密码,需在邮箱设置中生成)。
    EMAIL_HOST_PASSWORD = 'dxxxxxxg'
    # 默认发件人显示名称(可设为与 EMAIL_HOST_USER 相同)
    DEFAULT_FROM_EMAIL = 'xxxxx@qq.com'
    

    关于上面相关配置的参数,可以在管理服务里面找到

    然后找到配置方法

    这是相关文档:SMTP/IMAP服务

    编写发送邮箱的后端代码

    def send_email_captcha(request):
        # 使用get请求传参:?email=xxx
        email = request.GET.get("email")
        if not email:
            return JsonResponse({"code": 400, "message": "必须传递邮箱!"})
        # 生成验证码(取随机的4位阿拉伯数字)
        # ['0', '2', '9', '8']
        captcha = "".join(random.sample(string.digits, 4))
        send_mail(
            # subject(邮件主题)
            "LinNote注册验证码",
            # message(邮件正文)
            message=f"您的注册验证码是:{captcha}",
            #  recipient_list(收件人列表)
            # recipient_list=["user1@example.com", "user2@example.com"]  # 群发
            recipient_list=[email],
            # from_email(发件人地址),若为 None,则使用 settings.DEFAULT_FROM_EMAIL
            from_email=None,
        )
        return JsonResponse({"code": 200, "message": "邮箱验证码发送成功!"})
    

    存储验证码

    生成存储验证码的表

    注册APP

    INSTALLED_APPS = [
        "django.contrib.admin",
        "django.contrib.auth",
        "django.contrib.contenttypes",
        "django.contrib.sessions",
        "django.contrib.messages",
        "django.contrib.staticfiles",
        "blog.apps.BlogConfig", # 新注册
        "linauth.apps.LinauthConfig", # 新注册
    ]
    

    创建Model

    在linauth下面的model.py文件下创建models

    # Create your models here.
    class CaptchaModel(models.Model):
        email = models.EmailField(unique=True)
        captcha = models.CharField(max_length=4)
        create_time = models.DateTimeField(auto_now_add=True)
    

    运行迁移脚本创建表

    依次执行以下命令:

    python manage.py makemigrations
    
    python manage.py migrate
    

    我使用SQLite作数据库的原因

    条件 SQLite 优势
    🔹 轻量级服务器(只有 2G 内存) ✅ SQLite 不需要运行守护进程,占内存极小
    🔹 网站初期流量不大 ✅ 单文件数据库,足够应对中小型项目
    🔹 快速开发、部署 ✅ 零配置,开箱即用,Django 默认就支持
    🔹 个人项目或学习用途 ✅ 简单方便,维护成本低

    修改代码

    def send_email_captcha(request):
        email = request.GET.get("email")
        if not email:
            return JsonResponse({"code": 400, "message": "必须传递邮箱!"})
        captcha = "".join(random.sample(string.digits, 4))
        # 存储到数据库中
        """
        email=email: 数据库中查找 email 字段等于变量 email 值的记录
        defaults={"captcha": captcha}: 指定要创建或更新的字段值
            - 如果找到匹配记录:更新该记录的 captcha 字段值为变量 captcha 的值
            - 如果没找到:创建新记录,设置 email 和 captcha 字段的值
        """
        CaptchaModel.objects.update_or_create(email=email, defaults={"captcha": captcha})
        send_mail(
            "LinNote注册验证码",
            message=f"您的注册验证码是:{captcha}",
            recipient_list=[email],
            from_email=None,
        )
        return JsonResponse({"code": 200, "message": "邮箱验证码发送成功!"})
    

    获取验证码功能

    引入JQuery

    {% block head %}
        <script src="{% static 'jquery/jquery-3.7.1.min.js' %}"></script>
    {% endblock %}
    

    给按钮定义ID

    <div class="input-group">
        <input type="text" class="form-control" name="captcha" placeholder="验证码"
            aria-label="Recipient's username" aria-describedby="button-addon2">
            {% comment %} captcha-btn 定义按钮的id,用于绑定点击事件 {% endcomment %}
        <button class="btn btn-outline-secondary" type="button" id="captcha-btn">获取验证码</button>
    </div>
    

    编写JQuery代码

    /*
    $(function () { ... }) 的含义
    这是 jQuery 的文档就绪函数(document ready)的简写形式,完整写法是 $(document).ready(function(){...})。它的作用是:
        - 等待 DOM 完全加载:确保页面所有 HTML 元素都加载完成后再执行内部的 JavaScript 代码
        - 避免操作未加载的元素:防止脚本在 DOM 元素尚未存在时就尝试操作它们
        - 相当于原生 JS 的:window.onload = function(){...},但 jQuery 的版本更高效
    */
    $(function () {
        // 定义一个绑定函数
        function bindCaptchaBtnClick() {
            /*
            关于 $("#captcha-btn").click(function (event) { ... }) 的执行机制:
                - 绑定阶段(立即执行)
                    - 当代码运行到 $("#captcha-btn").click(...) 时,
                        jQuery 会立即绑定点击事件处理函数(即 function(event) { ... } 里的代码)。
                    - 但此时函数内部的代码不会执行,只是注册了一个回调函数,
                        告诉浏览器:"当 #captcha-btn 被点击时,执行这个函数"。
    
                - 触发阶段(用户点击后执行)
                    - 只有当用户真正点击了 #captcha-btn 按钮时,绑定的函数才会被执行。
                    - 此时会传入一个 event 对象(包含点击事件的相关信息,如触发元素、坐标等)。
            */
            $("#captcha-btn").click(function (event) {
                let $this = $(this);
                let email = $("input[name='email']").val();
                if (!email) {
                    alert("请先输入邮箱!");
                    return;
                }
                // 取消按钮的点击事件
                $this.off('click');
    
                // 发送ajax请求
                $.ajax('/auth/captcha?email=' + email, {
                    // 发送ajax请求
                    method: 'GET',
                    // 发送成功的回调函数
                    success: function (result) {
                        if (result['code'] == 200) {
                            alert("验证码发送成功!");
                        } else {
                            alert(result['message']);
                        }
                    },
                    // 发送失败的回调函数
                    fail: function (error) {
                        // console.log(error);
                    }
                })
    
                // 倒计时
                let countdown = 60;
                let timer = setInterval(function () {
                    if (countdown <= 0) {
                        $this.text('获取验证码');
                        // 清掉定时器
                        clearInterval(timer);
                        // 重新绑定点击事件
                        bindCaptchaBtnClick();
                    } else {
                        countdown--;
                        $this.text(countdown + "s")
                    }
                }, 1000);
            })
        }
        // 执行邦迪函数
        bindCaptchaBtnClick();
    });
    

    引入自己编写的JS代码

    {% block head %}
        <script src="{% static 'jquery/jquery-3.7.1.min.js' %}"></script>
        <script src="{% static 'js/register.js' %}"></script>
    {% endblock %}
    

    实现注册功能

    创建表单模型

    from django import forms
    from django.contrib.auth import get_user_model
    from .models import CaptchaModel
    
    # User = get_user_model() # 也可以启用,但是不推荐
    
    
    class RegisterForm(forms.Form):
        username = forms.CharField(
            max_length=20,
            min_length=2,
            error_messages={
                "required": "请传入用户名!",
                "max_length": "用户名长度在2~20之间!",
                "min_length": "用户名长度在2~20之间!",
            },
        )
        email = forms.EmailField(
            error_messages={"required": "请传入邮箱!", "invalid": "请传入一个正确的邮箱!"}
        )
        captcha = forms.CharField(max_length=4, min_length=4)
        password = forms.CharField(max_length=20, min_length=6)
    
        # 验证邮箱是否已经被注册
        def clean_email(self):
            email = self.cleaned_data.get("email")
            # 获取用户模型
            User = get_user_model()
            exists = User.objects.filter(email=email).exists()
            if exists:
                raise forms.ValidationError("邮箱已经被注册!")
            return email
    
        # 验证码是否和邮箱是匹配的
        def clean_captcha(self):
            captcha = self.cleaned_data.get("captcha")
            email = self.cleaned_data.get("email")
            captcha_model = CaptchaModel.objects.filter(
                email=email, captcha=captcha
            ).first()
            # 如果数据库里面不存在,说明验证码和邮箱不匹配
            if not captcha_model:
                raise forms.ValidationError("验证码和邮箱不匹配!")
    
            captcha_model.delete()
            return captcha
    

    解释User = get_user_model()

    get_user_model() 返回的是什么

    get_user_model()返回的是 Django 项目中配置的 用户模型类(即 settings.AUTH_USER_MODEL 指定的模型类)

    User是一个 Django Model 类(继承自 AbstractUserAbstractBaseUser),类似于你在 models.py 中定义的其他模型类(如 ProductOrder 等)。

    • 不是具体的用户对象(如 User.objects.get(id=1) 返回的实例)。
    • 不是数据库中的数据(需要通过 User.objects 查询才能获取数据)。

    为什么不直接引用

    为什么自定义模型可以直接导入,而用户模型需要 get_user_model()

    自定义模型(如 CaptchaModel

    • 明确绑定到你的应用:当你从 .models 导入 CaptchaModel 时,你明确知道它是当前项目定义的模型,不存在替换需求。
    • 无需动态切换:它的定义是静态的,不会因为 Django 配置而变化。

    用户模型(User)

    • Django 允许完全替换默认用户模型:通过 settings.AUTH_USER_MODEL 可以指定自定义用户模型(例如添加 phone 字段或改用邮箱登录)。
    • 需要动态获取当前配置的模型:直接导入 from django.contrib.auth.models import User 会硬编码默认模型,导致无法兼容自定义用户模型。

    get_user_model() 的作用

    动态获取当前用户模型

    from django.contrib.auth import get_user_model
    User = get_user_model()  #  自动返回 CustomUser 或默认 User
    
    • 如果 AUTH_USER_MODEL = 'myapp.CustomUser',则 User 指向 CustomUser
    • 如果未配置,则返回默认 django.contrib.auth.models.User

    为什么不能提前赋值?

    User = get_user_model()  # 模块级别赋值(不推荐)
    class RegisterForm(forms.Form):
        username = forms.CharField(
          ......
        )
        ......
    
    • 问题:Django 启动时模型可能未加载完成,导致 AppRegistryNotReady 错误。

    如何auth的User不满足业务需求怎么办

    如果项目需要扩展 Django 默认的 User 模型(例如添加 phone 字段),Django 提供了两种推荐方案。以下是详细解决方案:

    特性 AbstractUser AbstractBaseUser
    保留默认字段 (username/password等) (完全自定义)
    开发复杂度 高(需实现用户管理器)
    适用场景 添加字段/微调默认行为 完全重构认证逻辑(如手机号登录)

    选择建议:优先使用 AbstractUser,除非你需要彻底重写用户系统。

    auth模块的优势

    为什么一定要嵌入auth模块?为什么不自己写一个User类,然后自己校验密码?

    auth有以下一些优势:

    功能 自定义独立模型 继承 Django Auth
    密码加密与验证 需手动实现 内置 set_password()/check_password()
    登录会话管理(login()/logout() 需手动实现 直接使用 django.contrib.auth
    Admin 后台集成 需重写 自动支持
    权限系统(Groups/Permissions) 需重写 内置支持
    第三方包兼容性(如 DRF) 需适配 开箱即用

    后端代码

    @require_http_methods(["GET", "POST"])
    def register(request):
        if request.method == "GET":
            return render(request, "register.html")
        else:
            form = RegisterForm(request.POST)
            if form.is_valid():
                email = form.cleaned_data.get("email")
                username = form.cleaned_data.get("username")
                password = form.cleaned_data.get("password")
                User = get_user_model()
                # 存入password是密文,是Django的auth模块底层设置的
                User.objects.create_user(email=email, username=username, password=password)
                # 从定向到登录页面
                return redirect(reverse("linauth:login"))
            else:
                print(form.errors)
                # 重新跳转到登录页面
                return redirect(reverse("linauth:register"))
    

    添加csrf的token

    <form action="" method="POST">
        {% csrf_token %}
        <div class="mb-3">
            <input type="text" name="username" class="form-control" placeholder="用户名">
        </div>
    		......
    </form>
    

    实现登陆功能

    登陆表单的Form

    class LoginForm(forms.Form):
        email = forms.EmailField(
            error_messages={"required": "请传入邮箱!", "invalid": "请传入一个正确的邮箱!"}
        )
        password = forms.CharField(max_length=20, min_length=6)
        remember = forms.IntegerField(required=False)
    

    添加csrf的token

    <form action="" method="POST">
        {% csrf_token %}
        <div class="mb-3">
            <input type="email" name="email" class="form-control" placeholder="邮箱">
        </div>
        ......
    </form>
    

    编写后端验证逻辑

    @require_http_methods(["GET", "POST"])
    def linlogin(request):
        if request.method == "GET":
            return render(request, "login.html")
        else:
            form = LoginForm(request.POST)
            if form.is_valid():
                email = form.cleaned_data.get("email")
                password = form.cleaned_data.get("password")
                remember = form.cleaned_data.get("remember")
                User = get_user_model()
                user = User.objects.filter(email=email).first()
                # 验证用户输入的明文密码(password)是否与数据库中存储的加密密码匹配。
                # 返回值是一个布尔值(True 或 False)
                if user and user.check_password(password):
                    # 登录:Django的登录
                    login(request, user)
                    # 判断是否需要记住我:remember=0 为false 其他值为true
                    if not remember:
                        # 如果没有点击记住我,那么就要设置过期时间为0,即浏览器关闭后就会过期
                        request.session.set_expiry(0)
                    # 如果点击了,那么就什么都不做,使用默认的2周的过期时间
                    return redirect("/")
                else:
                    print("邮箱或密码错误!")
                    # form.add_error('email', '邮箱或者密码错误!')
                    # return render(request, 'login.html', context={"form": form})
                    return redirect(reverse("linauth:login"))
    

    login(request, user) 干了哪些事情

    作用

    • 将用户实例(user)绑定到当前会话(Session),标记用户为“已认证状态”。
    • 使得后续请求可以通过 request.user 获取当前登录用户。

    内部执行流程

    1. 会话创建
      • request.session 中存储用户的唯一标识(通常是 user.id)。
      • Django 默认使用数据库存储会话(可通过 settings.SESSION_ENGINE 配置为缓存或其他后端)。
    2. 用户标记
      • user 对象赋值给 request.user,供后续请求使用。
      • 如果启用了 django.contrib.auth.middleware.AuthenticationMiddleware(默认启用),每个请求的 request.user 会自动从会话中恢复。
    3. 可选功能
      • 更新用户的 last_login 字段(如果用户模型继承自 AbstractBaseUser)。
      • 触发 user_logged_in 信号(允许其他组件监听登录事件)。

    登录和注册按钮的跳转

    <div class="text-end"> 
        {% comment %} 注意,这里的标签从button变成 a ; 也就是从按钮变成超链接{% endcomment %}
        <a href="{% url 'linauth:login' %}" type="button" class="btn btn-outline-primary">登录</a> 
        <a href="{% url 'linauth:register' %}" type="button" class="btn btn-primary">注册</a> 
    </div>
    

    登录和非登录状态的切换

    {% comment %} 
    user.is_authenticated 是jango.contrib.auth.models.User类的一个属性
    它可以判断,当前用户是否登录
    {% endcomment %}
    {% if user.is_authenticated %}
        {% comment %} 如果登录了,就显示用户的头像 {% endcomment %}
        <!-- 下面是bootstrap的一个样式 -->
        <div class="dropdown text-end">
            <a href="#" class="d-block link-body-emphasis text-decoration-none dropdown-toggle"
            data-bs-toggle="dropdown" aria-expanded="false">
                <img src="{% static 'image/avatar.jpg' %}" alt="mdo" width="32" height="32" class="rounded-circle">
            </a>
            <ul class="dropdown-menu text-small" style="">
                <li><a class="dropdown-item" href="{% url 'linauth:logout' %}">退出登录</a></li>
            </ul>
        </div>
    {% else %}
        {% comment %} 如果没有登录,就显示登录和注册两个按钮 {% endcomment %}
        <!-- primary:蓝色 -->
        <div class="text-end"> 
            {% comment %} 注意,这里的标签从button变成 a {% endcomment %}
            <a href="{% url 'linauth:login' %}" type="button" class="btn btn-outline-primary">登录</a> 
            <a href="{% url 'linauth:register' %}" type="button" class="btn btn-primary">注册</a> 
        </div>
    {% endif %}
    

    user.is_authenticated

    是什么

    user.is_authenticated 是 Django 用户模型(User Model)的一个**属性,用于判断当前用户是否已通过认证(即是否已登录)。

    • 返回值

      • True → 用户已登录(认证通过)。

        class User(AbstractUser):
            class Meta(AbstractUser.Meta):
                swappable = "AUTH_USER_MODEL"
                
        class AbstractUser(AbstractBaseUser, PermissionsMixin):
            username_validator = UnicodeUsernameValidator()
            ......
            
            
        class AbstractBaseUser(models.Model):
            @property
            def is_authenticated(self):
                """
                Always return True. This is a way to tell if the user has been
                authenticated in templates.
                """
                return True
        
      • False → 用户未登录(匿名用户)。

        class AnonymousUser:
            @property
            def is_authenticated(self):
                return False
        

    为什么能在模板中直接使用 user.is_authenticated

    Django 模板自动注入 user 对象

    只要使用了 django.contrib.auth.context_processors.auth 上下文处理器(默认启用),Django 会在渲染模板时自动将 request.user 注入为模板变量 user。无需手动传递 user 到模板上下文。

    TEMPLATES = [
        {
            "BACKEND": "django.template.backends.django.DjangoTemplates",
            "DIRS": [BASE_DIR / "templates"],
            "APP_DIRS": True,
            "OPTIONS": {
                "context_processors": [
                    "django.template.context_processors.debug",
                    "django.template.context_processors.request",
                    "django.contrib.auth.context_processors.auth", ### 这里使用了
                    "django.contrib.messages.context_processors.messages",
                ],
                # 加在这里
                "builtins": ["django.templatetags.static"],
            },
        },
    ]
    

    退出登录功能

    后端代码

    from django.contrib.auth import logout
    def linlogout(request):
        # 调用Django的退出登录功能
        logout(request)
        # 重定向到首页
        return redirect("/")
    

    前端代码

    <ul class="dropdown-menu text-small" style="">
        <li><a class="dropdown-item" href="{% url 'linauth:logout' %}">退出登录</a></li>
    </ul>
    

    logout干了些什么

    作用

    • 终止当前用户的会话,清除认证状态。
    • 确保用户无法继续通过 request.user 访问受保护资源。

    内部执行流程

    1. 会话销毁
      • 清除 request.session 中的所有数据(包括用户 ID)。
      • 如果使用数据库会话,会删除对应的 django_session 表记录。
    2. 用户标记清除
      • request.user 设置为匿名用户(AnonymousUser 实例)。
    3. 可选功能
      • 触发 user_logged_out 信号(允许其他组件监听登出事件)。

    实现博客发布功能

    发布博客访问限制

    点击发布博客按钮,后端会验证目前是否登录,如果没有登录,就跳转到登录页面,否则就跳转到博客发布页面。

    前端引入跳转的URL

    <ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
        <li><a href="#" class="nav-link px-2 text-secondary">首页</a></li>
        {% comment %} 跳转到发布博客页面 {% endcomment %}
        <li><a href="{% url 'blog:pub_blog' %}" class="nav-link px-2 text-secondary">发布博客</a></li>
    </ul>
    

    后端校验是否登录

    方式一:直接写死url

    # 如果没有登录,会跳转到 /auth/login
    from django.contrib.auth.decorators import login_required
    @login_required(login_url="/auth/login")
    @require_http_methods(["GET", "POST"])
    def pub_blog(request):
        if request.method == "GET":
            # categories = BlogCategory.objects.all()
            return render(request, "pub_blog.html")
    

    方式二:使用懒加载

    from django.contrib.auth.decorators import login_required
    from django.urls.base import reverse_lazy
    # 这里使用了 App名(linauth)+url名(login)的配置
    # 如果不使用懒加载,使用reverse作翻转,
    # @login_required(login_url=reverse("linauth:login"))
    # 那么,在reverse作翻转的时候,很可能会因为Django无法读到urls.py的配置,而无法获取url
    # 那么,就可以使用懒加载reverse_lazy,等到配置都读完了,在加载url
    @login_required(login_url=reverse_lazy("linauth:login"))
    @require_http_methods(["GET", "POST"])
    def pub_blog(request):
        if request.method == "GET":
            # categories = BlogCategory.objects.all()
            return render(request, "pub_blog.html")
    

    方式三:使用@login_required() + settings.py的配置

    直接使用@login_required()

    from django.contrib.auth.decorators import login_required
    @require_http_methods(["GET", "POST"])
    @login_required()
    def pub_blog(request):
        if request.method == "GET":
            # categories = BlogCategory.objects.all()
            return render(request, "pub_blog.html")
    

    然后在settings.py的配置

    LOGIN_URL = "/auth/login"
    

    @login_required 检查用户登录状态的步骤

    在请求到达视图前,Django 的中间件会按顺序处理 request 对象:

    1. SessionMiddleware
      • 从请求的 Cookie 中提取 sessionid(如 sessionid=abc123)。
      • 通过 sessionid 查询 django_session 表,加载会话数据到 request.session(类似字典)。
    2. AuthenticationMiddleware
      • request.session 中读取用户 ID(键为 _auth_user_id)。
      • 用该 ID 从 auth_user 表查询用户,并赋值给 request.user
      • 如果未找到 ID,request.user 设为 AnonymousUser
    3. 检查 request.user
      • 已登录用户 → request.userUser 实例(如 admin)。
      • 未登录用户 → request.userAnonymousUser 实例。
    4. 验证 is_authenticated
      • 调用 request.user.is_authenticated
        • True → 放行,执行视图函数。
        • False → 触发跳转。
    5. 未登录则跳转到登录页
      • 默认跳转至 settings.LOGIN_URL(如 /auth/login/)。
      • 携带 next 参数(如 ?next=/profile/),登录后自动返回原页面。

    创建相关模型

    from django.db import models
    from django.contrib.auth import get_user_model
    
    
    class BlogCategory(models.Model):
        name = models.CharField(max_length=200)
    
    
    class Blog(models.Model):
        title = models.CharField(max_length=200)
        content = models.TextField()
        pub_time = models.DateTimeField(auto_now_add=True)
        category = models.ForeignKey(
            BlogCategory, on_delete=models.CASCADE
        )
        User = get_user_model()
        author = models.ForeignKey(User, on_delete=models.CASCADE)
    
    
    class BlogComment(models.Model):
        content = models.TextField()
        pub_time = models.DateTimeField(auto_now_add=True)
        blog = models.ForeignKey(
            Blog, on_delete=models.CASCADE
        )
        User = get_user_model()
        author = models.ForeignKey(User, on_delete=models.CASCADE)
    

    python manage.py makemigrations

    python manage.py migrate

    Django的Admin系统的使用

    登录

    auth_user表中is_staff 字段值为1的用户才可以登录Admin。因此需要把该值改成1

    登录之后,发现自己并没有权限

    需要把auth_user表中的is_superuser的值改成1之后,才会有权限

    改成中文:在settings.py文件下修改LANGUAGE_CODE

    LANGUAGE_CODE = "zh-hans"
    

    创建superuser

    也可以通过创建superuser来实现这个功能:python manage.py createsuperuser

    (.venv) (base) xxx@xxxdeMacBook-Pro LinNote % python manage.py createsuperuser
    Username (leave blank to use 'xxx'): xieshaolin
    Email address: xxx@163.com
    Password: 
    Password (again): 
    

    添加blog的相关配置

    添加admin配置

    blogapp下的admin.py文件里面添加如下配置

    from django.contrib import admin
    
    # Register your models here.
    from .models import BlogCategory, Blog, BlogComment
    
    
    class BlogCategoryAdmin(admin.ModelAdmin):
        list_display = ["name"]
    
    
    class BlogAdmin(admin.ModelAdmin):
        list_display = ["title", "content", "pub_time", "category", "author"]
    
    
    class BlogCommentAdmin(admin.ModelAdmin):
        list_display = ["content", "pub_time", "author", "blog"]
    
    # 绑定 模型 和 admin 的管理项
    admin.site.register(BlogCategory, BlogCategoryAdmin)
    admin.site.register(Blog, BlogAdmin)
    admin.site.register(BlogComment, BlogCommentAdmin)
    

    把配置项的名字改成中文

    from django.db import models
    from django.contrib.auth import get_user_model
    
    
    class BlogCategory(models.Model):
        name = models.CharField(max_length=200)
    
        class Meta:
            # apple, apples
            verbose_name = "博客分类"
            # 因为在英语中有复数这个概念,如apple, apples
            # 如果不加上这个配置,那么遇到复数的形式,"博客分类"会变成"博客分类s"
            verbose_name_plural = verbose_name
    
    
    class Blog(models.Model):
        title = models.CharField(max_length=200)
        content = models.TextField()
        pub_time = models.DateTimeField(auto_now_add=True)
        category = models.ForeignKey(BlogCategory, on_delete=models.CASCADE)
        User = get_user_model()
        author = models.ForeignKey(User, on_delete=models.CASCADE)
    
        class Meta:
            # apple, apples
            verbose_name = "博客"
            verbose_name_plural = verbose_name
    
    
    class BlogComment(models.Model):
        content = models.TextField()
        pub_time = models.DateTimeField(auto_now_add=True)
        blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
        User = get_user_model()
        author = models.ForeignKey(User, on_delete=models.CASCADE)
    
        class Meta:
            # apple, apples
            verbose_name = "评论"
            verbose_name_plural = verbose_name
    

    修改完之后,博客的配置项已经变成中文了,但是博客的配置属性还没有变成中文

    把配置属性变成中文

    在属性里面添加verbose_name

    from django.db import models
    from django.contrib.auth import get_user_model
    
    
    class BlogCategory(models.Model):
        name = models.CharField(max_length=200, verbose_name="分类名称")
    
        class Meta:
            # apple, apples
            verbose_name = "博客分类"
            # 因为在英语中有复数这个概念,如apple, apples
            # 如果不加上这个配置,那么遇到复数的形式,"博客分类"会变成"博客分类s"
            verbose_name_plural = verbose_name
    
    
    class Blog(models.Model):
        title = models.CharField(max_length=200, verbose_name="标题")
        content = models.TextField(verbose_name="内容")
        pub_time = models.DateTimeField(auto_now_add=True, verbose_name="发布时间")
        category = models.ForeignKey(
            BlogCategory, on_delete=models.CASCADE, verbose_name="分类"
        )
        User = get_user_model()
        author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="作者")
    
        class Meta:
            # apple, apples
            verbose_name = "博客"
            verbose_name_plural = verbose_name
    
    
    class BlogComment(models.Model):
        content = models.TextField(verbose_name="内容")
        pub_time = models.DateTimeField(auto_now_add=True, verbose_name="发布时间")
        blog = models.ForeignKey(
            Blog, on_delete=models.CASCADE, verbose_name="所属博客"
        )
        User = get_user_model()
        author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="作者")
    
        class Meta:
            # apple, apples
            verbose_name = "评论"
            verbose_name_plural = verbose_name
    

    定义__str__(self)

    from django.db import models
    from django.contrib.auth import get_user_model
    
    
    class BlogCategory(models.Model):
        name = models.CharField(max_length=200, verbose_name="分类名称")
    
        def __str__(self):
            return self.name
    
        class Meta:
            # apple, apples
            verbose_name = "博客分类"
            # 因为在英语中有复数这个概念,如apple, apples
            # 如果不加上这个配置,那么遇到复数的形式,"博客分类"会变成"博客分类s"
            verbose_name_plural = verbose_name
    
    
    class Blog(models.Model):
        title = models.CharField(max_length=200, verbose_name="标题")
        content = models.TextField(verbose_name="内容")
        pub_time = models.DateTimeField(auto_now_add=True, verbose_name="发布时间")
        category = models.ForeignKey(
            BlogCategory, on_delete=models.CASCADE, verbose_name="分类"
        )
        User = get_user_model()
        author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="作者")
    
        def __str__(self):
            return self.title
    
        class Meta:
            # apple, apples
            verbose_name = "博客"
            verbose_name_plural = verbose_name
    
    
    class BlogComment(models.Model):
        content = models.TextField(verbose_name="内容")
        pub_time = models.DateTimeField(auto_now_add=True, verbose_name="发布时间")
        blog = models.ForeignKey(
            Blog, on_delete=models.CASCADE, verbose_name="所属博客"
        )
        User = get_user_model()
        author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="作者")
    
        def __str__(self):
            return self.content
    
        class Meta:
            # apple, apples
            verbose_name = "评论"
            verbose_name_plural = verbose_name
    

    在 Django Admin 中的表现

    后台列表页(List View)

    • 默认行为:如果未定义 __str__,Django Admin 会显示对象的默认字符串表示(如 BlogCategory object (1)),这对管理员毫无意义。
    • 定义 __str__:在列表页中会直接显示 name 字段的值(如 "技术""生活"),清晰易读。

    下拉选择框(ForeignKey/ManyToManyField)

    • 当其他模型通过 ForeignKeyManyToManyField 关联到 BlogCategory 时:
      • 在 Admin 的表单中,下拉选择框会显示 __str__ 返回的值(如 "技术"),而非 object(1)

    其他 Django 场景

    Shell或日志调试

    • 在 Django Shell (python manage.py shell) 中查询模型时:

      >>> category = BlogCategory.objects.first()
      >>> print(category)  # 输出 "技术"(而非无意义的 `BlogCategory object (1)`)
      

    模板渲染

    • 在模板中直接输出对象时:

      {{ category }}  <!-- 显示分类名称,如 "技术" -->
      

    为什么不用 verbose_name

    • verbose_name模型的显示名称(用于 Admin 的标题、表单标签等),而 __str__对象实例的显示名称
    • 例如:
      • verbose_name 控制的是“博客分类”这个类别的标题。
      • __str__ 控制的是具体某个分类(如 "技术")的显示。

    发布博客后端代码

    # 以下是form表达的模型
    from django import forms
    class PubBlogForm(forms.Form):
        title = forms.CharField(max_length=200, min_length=2)
        content = forms.CharField(min_length=2)
        category = forms.IntegerField()
    
    
    
    
    # 以下是业务代码
    @require_http_methods(["GET", "POST"])
    @login_required()
    def pub_blog(request):
        # 如果是Get请求,说明只是访问发布博客
        if request.method == "GET":
            # 查询所有分类
            categories = BlogCategory.objects.all()
            # 把分类传给模版
            return render(request, "pub_blog.html", context={"categories": categories})
        # 如果是post请求,说明是在提交表达
        else:
            form = PubBlogForm(request.POST)
            if form.is_valid():
                title = form.cleaned_data.get("title")
                content = form.cleaned_data.get("content")
                category_id = form.cleaned_data.get("category")
                blog = Blog.objects.create(
                    title=title,
                    content=content,
                    category_id=category_id,
                    author=request.user,
                )
                return JsonResponse(
                    # 这里给前端返回一个blog.id的目的是为了发生成功之后,跳转的博客详情页
                    {"code": 200, "message": "博客发布成功!", "data": {"blog_id": blog.id}}
                )
            else:
                print(form.errors)
                return JsonResponse({"code": 400, "message": "参数错误!"})
    

    发布博客的前端内容

    HTML的内容

    分类下拉框

    <!-- 引入 这里引入id,那么js就更容易获取数据 -->
    <select name="category" class="form-select" id="category-select">
        {% for category in categories %}
            <option value="{{ category.id }}">{{ category.name }}</option>
        {% endfor %}
    </select>
    

    引入jquery

    pub_blog.html里面引入jquery

    script src="{% static 'jquery/jquery-3.7.1.min.js' %}"></script>
    

    给按钮绑定Id

    <div class="mb-3 text-end">
        <button class="btn btn-primary" id="submit-btn">发布</button>
    </div>
    

    补充csrf_token

    <!-- 引入 csrf_token -->
    {% csrf_token %}
    

    Js绑定点击事件

    $("#submit-btn").click(function (event) {
        /*阻止按钮的默认行为: 
            如果不阻止默认行为,点击发布按钮,就会直接通过表单的形式发布给后端,
            但是富文本框里面的内容,是不能通过表单提交的*/
        event.preventDefault();
        // 找到input标签里面 name='title' 的 要素,然后 .val() 获取它的值
        let title = $("input[name='title']").val();
        // 找到id=category-select 的要是,然后 .val() 获取它的值
        // 用id的方式,要比前面那种方式快,因为id唯一
        let category = $("#category-select").val();
        //  这个方法只能获取文本,不能获取样式,如一级标题,二级标题
        // let content= editor.getText(); 
        // 这个方法可以获取样式
        let content = editor.getHtml();
        /**
         *  获取CSRF的token:
         *  使用{% csrf_token %} 渲染的结果是:
         *  <input type="hidden" name="csrfmiddlewaretoken" value="4nTc...A">
         */
        let csrfmiddlewaretoken = $("input[name='csrfmiddlewaretoken']").val();
        // 实际上只有两个参数:参数1 url=/blog/pub'; 参数2 opotion={}
        $.ajax('/blog/pub', {
            // 请求的方式
            method: 'POST',
            // 上传的数据
            data: { title, category, content, csrfmiddlewaretoken },
            success: function (result) {
                if (result['code'] == 200) {
                    // 获取博客id
                    let blog_id = result['data']['blog_id']
                    // 跳转到博客详情页面
                    window.location = '/blog/detail/' + blog_id
                } else {
                    alert(result['message']);
                }
            }
        })
    });
    

    展示博客详情

    后端

    def blog_detail(request, blog_id):
        try:
            blog = Blog.objects.get(pk=blog_id)
        except Exception as e:
            print(e)
        return render(request, "blog_detail.html", context={"blog": blog})
    

    前端

    <h1>{{blog.title}}</h1>
    <!-- hr: 会显示一条横线 -->
    <hr />
    <!-- mt-2: spacing内容,t - for classes that set margin-top or padding-top -->
    <div class="mt-2">
      <img src="{% static 'image/avatar.jpg'%}" class="rounded-circle" width="30" height="30">
      <!-- s - (start) for classes that set margin-left or padding-left in LTR, margin-right or padding-right in RTL -->
      <span class="ms-2">{{ blog.author.username }}</span>
      <span class="ms-2">于</span>
      {% comment %} date过滤器设置时间格式 {% endcomment %}
      <span class="ms-2">{{ blog.pub_time|date:"Y年m月d日 h时i分" }}</span>发布
    </div>
    <hr />
    <!-- y - for classes that set both *-top and *-bottom -->
    <!-- py-2: 上下内边距调整2 -->
    <div class="py-2">
      {% comment %} 标记这个字符串是安全的,让他渲染成HTML {% endcomment %}
       {{ blog.content|safe }}
    </div>
    

    评论功能

    后端

    # 增加related_name属性
    class BlogComment(models.Model):
        content = models.TextField(verbose_name="内容")
        pub_time = models.DateTimeField(auto_now_add=True, verbose_name="发布时间")
        blog = models.ForeignKey(
            Blog,
            on_delete=models.CASCADE,
            # related_name="comments":设置这个属性就可以通过 Blog.comments查到这个blog下面的所有评论
            related_name="comments",
            verbose_name="所属博客",
        )
        User = get_user_model()
        author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="作者")
    
    # 发布comment的逻辑
    @require_POST
    @login_required()
    def pub_comment(request):
        blog_id = request.POST.get("blog_id")
        content = request.POST.get("content")
        #  author=request.user: 会自动提取user的id
        BlogComment.objects.create(content=content, blog_id=blog_id, author=request.user)
        # 重新加载博客详情页
        # kwargs可以传参:/blog/detail/{blog_id}
        return redirect(reverse("blog:blog_detail", kwargs={"blog_id": blog_id}))
    

    前端

    <div class="mt-2">
        {% comment %} 相当于后端代码的: blog.comments.all() 方法{% endcomment %}
        {% comment %} 
        下面的评论列表是访问/blog/detail/{blog_id}的时候就需要显示
            所以只给前端上传了 context={"blog": blog}
        {% endcomment %}
        <h3>评论({{ blog.comments.all|length }})</h3>
        {% comment %} 表单提交地址 {% endcomment %}
        <form action="{% url 'blog:pub_comment' %}" method="POST">
            {% comment %} 添加csrf_token {% endcomment %}
            {% csrf_token %}
            {% comment %} 添加blog_id,用于刷新页面 {% endcomment %}
            <input type="hidden" name="blog_id" value="{{ blog.id }}">  
            <div class="mt-2">
                 {% comment %} 新添 name="content" {% endcomment %}
                <input type="text" class="form-control" placeholder="请输入评论" name="content">
            </div>
            <div class="text-end mt-2">
                 {% comment %} 改成submit,表单才可以提交 {% endcomment %}
                 <!-- <button type="submit" class="btn btn-primary">评论</button> -->
                <button type="submit" class="btn btn-primary">评论</button>
            </div>
        </form>
    </div>
    <div class="mt-2">
        <!-- list-group list-group-flush: bootstrap的列表样式 -->
        <ul class="list-group list-group-flush">
            {% for comme in blog.comments.all %}
            <li class="list-group-item">
                <div class="d-flex justify-content-between text-secondary">
                    <div class="user-info">
                        <img src="{% static 'image/avatar.jpg'%}" class="rounded-circle" width="30" height="30">
                        <span class="ms-2">{{ comme.author.username }}</span>
                    </div>
    
                   <div class="create-time" style="line-height: 40px;">{{ comme.pub_time|date:"Y年m月d日 h时i分" }}</div>
                </div>
                <div style="padding-left: 30px;">
                    <div class="ms-2">{{ comme.content }}</div>
                </div>
            </li>
            {% endfor %}
        </ul>
    </div>
    

    代码高亮Highlight.js

    Highlight.js 是一个轻量级、高性能的代码语法高亮库,用于在网页中高亮显示各种编程语言的代码片段。

    1. 下载Highlight.js

    2. 映入Highlight

      {% block head %}
          {% comment %} 引入主题 {% endcomment %}
          <link rel="stylesheet" href="{% static 'highlight/styles/github-dark.min.css' %}">
          {% comment %} 引入hightlight.js {% endcomment %}
          <script src="{% static 'highlight/highlight.min.js' %}"></script>
      {% endblock %}
      
    3. 高亮代码

      {% comment %} 
      hljs
      Highlight.js 库的全局对象,所有功能通过它调用。
      
      .highlightAll()该方法会:
          扫描整个网页。
          找到所有 <pre><code> 标签(或带 class="hljs" 的代码块)。
          根据代码语言(如 class="language-python")自动应用语法高亮。
          如果未指定语言,尝试自动检测。
      
      这行代码是 Highlight.js 的“一键高亮”开关,自动为页面中的所有代码块着色,无需手动逐个处理。
      
      放在代码最后面也是希望所有要素都加载完毕后,高亮所有代码
      {% endcomment %}
      <script>hljs.highlightAll(); </script>
      

    首页功能

    后端代码

    # 显示所有文章
    def index(request):
        blogs = Blog.objects.all()
        return render(request, "index.html", context={"blogs": blogs})
    

    后端模版

    <h1>博客列表</h1>
    <div class="row row-cols-2 row-gap-4">
        {% for blog in blogs %}
            <div class="col">
                <!-- text-center:文字居中,删除就不会文字居中了 -->
                <div class="card">
                    <div class="card-header">
                        <!-- 这里展示文章标题 -->
                         {% comment %} blog_id=blog.id 传递URL参数 {% endcomment %}
                        <a href="{% url 'blog:blog_detail' blog_id=blog.id %}">{{ blog.title }}</a>
                    </div>
                    <!-- height: 100px:设置card-body的固定高度 -->
                    <div class="card-body" style="height: 100px;">
                        <!-- 原来的标题不用了 -->
                        <!-- <h5 class="card-title"></h5> -->
                         {% comment %} striptags:去除HTML格式 {% endcomment %}
                         {% comment %} truncatechars:截取100个字符 {% endcomment %}
                        <p class="card-text">{{ blog.content|striptags|truncatechars:100 }}</p>
                    </div>
                    <!-- d-flex justify-content-between :设置d-flex -->
                    <div class="card-footer text-muted d-flex justify-content-between">
                        <div>
                            <img src="{% static 'image/avatar.jpg'%}" class="rounded-circle" width="30" height="30">
                            <!-- 用户名 -->
                            {{ blog.author.username }}
                        </div>
                        <div>
                            <!-- 先填一个假的发布时间 -->
                            发布时间:{{ blog.pub_time|date:"Y年m月d日 h时i分" }}
                        </div>
    
                    </div>
                </div>
            </div>
        {% endfor %}
    </div>
    

    查询功能

    后端代码

    @require_GET
    def search(request):
        # /search?q=xxx
        q = request.GET.get("q")
        # 从博客的标题和内容中查找含有q关键字的博客
        blogs = Blog.objects.filter(Q(title__icontains=q) | Q(content__icontains=q)).all()
        return render(request, "index.html", context={"blogs": blogs})
    

    前端代码

    <form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3" role="search" action="{% url 'blog:search' %}" method="GET"> 
        <input type="search" class="form-control"  name="q" placeholder="搜索..." aria-label="Search">
    </form>
    

    转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1909773034@qq.com

    ×

    喜欢就点赞,疼爱就打赏