Skip to content

Django搭建博客

12387字约41分钟

PythonDjango

2025-05-18

Django 搭建博客

0.环境说明

Python 3.12.4
Django 5.0.7

0.1虚拟环境配置

配置虚拟环境

cd DjangoProject
python -m venv .venv

激活虚拟环境

.\.venv\Scripts\activate

image-20240724110032873

0.2 Django 安装

使用 pip 安装 django 时建议指定 pypi 源,否则安装速度可能会很慢。

pip install django -i https://pypi.tuna.tsinghua.edu.cn/simple

安装完成后,使用 pip list 检查是否成功安装 Django

image-20240724110204647

0.3 Django 项目创建

这里需要激活虚拟环境后,再执行如下操作,创建 DjangoBlog 项目。

django-admin startproject DjangoBlog

创建完成后,会生成如下的目录结构。

DjangoBlog
  db.sqlite3
  manage.py

└─DjangoBlog
  settings.py
  urls.py
  wsgi.py
    └─ __init__.py

0.4 运行 Django 服务器

cd DjangoBlog
python manage.py runserver

项目运行成功后,访问 127.0.0.1:8000 即可查看到如下界面。

image-20240724110659851

1.article 文章管理应用

1.1 article 文章应用创建

激活虚拟环境后,在 DjangoBlog 目录下执行如下命令。

python manage.py startapp article

image-20240724132441480

操作完成后,DjangoBlog 的目录结构如下所示。

DjangoBlog
  db.sqlite3
  manage.py

├─article
  admin.py
  apps.py
  models.py
  tests.py
  views.py
  __init__.py

  └─migrations
        └─ __init__.py

└─DjangoBlog
  settings.py
  urls.py
  wsgi.py
    └─ __init__.py

1.2 article 文章应用注册

修改 DjangoBlog/DjangoBlog/settings.py 这个文件,在 INSTALLED_APPS 中添加 article

# DjangoBlog/DjangoBlog/settings.py
INSTALLED_APPS = [
    ...
    # 新增article应用
    'article'
]

1.3 artcile 文章应用路由

首先是修改 DjangoBlog/urls.py,新增如下内容:

# DjangoBlog/DjangoBlog/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    # 新增代码,配置app的url
    path('article/', include('article.urls', namespace='article')),
]

修改完成后,article 文章应用的路由地址为 /article

article 文章应用在创建的时候,并没有生成 urls.py,需要在 article 目录下新增 urls.py,其中的内容如下:

# article/urls.py

# 引入path
from django.urls import path

# 正在部署的应用的名称
app_name = 'article'

urlpatterns = [
    # 目前还没有urls
]

1.4 artcile 文章应用模型

Django 中通过模型(Model)映射到数据库,处理与数据相关的事务。

编写用于存储文章的 ArticlePost 数据模型,在 article/models.py 中编写如下内容。

# DjangoBlog/DjangoBlog/article/models.py
from django.db import models
# 导入内建的User模型。
from django.contrib.auth.models import User
# timezone 用于处理时间相关事务。
from django.utils import timezone

# 博客文章数据模型
class ArticlePost(models.Model):
    # 文章作者。参数 on_delete 用于指定数据删除的方式
    author = models.ForeignKey(User, on_delete=models.CASCADE)

    # 文章标题。models.CharField 为字符串字段,用于保存较短的字符串,比如标题
    title = models.CharField(max_length=100)

    # 文章正文。保存大量文本使用 TextField
    body = models.TextField()

    # 文章创建时间。参数 default=timezone.now 指定其在创建数据时将默认写入当前的时间
    created = models.DateTimeField(default=timezone.now)

    # 文章更新时间。参数 auto_now=True 指定每次数据更新时自动写入当前时间
    updated = models.DateTimeField(auto_now=True)
    
    # 内部类 class Meta 用于给 model 定义元数据
    class Meta:
    	# ordering 指定模型返回的数据的排列顺序
    	# '-created' 表明数据应该以倒序排列
        ordering = ('-created',)

    # 函数 __str__ 定义当调用对象的 str() 方法时的返回值内容
    def __str__(self):
    	# return self.title 将文章标题返回
        return self.title

执行 makemigrations 生成迁移文件。

python manage.py makemigrations

image-20240724200216290

执行 migrate ,将迁移内容应用到数据库中。

python manage.py migrate

image-20240724200231210

1.5 Django 网站后台管理

Django 内置了一个后台管理工具,只需要少量代码,就能够实现强大的功能。

1.5.1 创建管理员账号

管理员账号(Superuser)是可以进入网站后台,对数据进行维护的账号,具有很高的权限。

激活虚拟环境后,输入 python manage.py createsuperuser 创建管理员账号,过程中需要输入账号、密码、邮箱,如下所示。

image-20240724202025683

管理员账号创建完成后,浏览器输入 http://127.0.0.1:8000/admin/ 这个地址,就可进入 Django 后台管理界面,输入刚才创建的账号和密码,点击登录。

image-20240724220135574

image-20240724220202832

1.5.2 artcile 文章模型注册到后台

Django 网站后台管理能够实现对于数据表模型的管理,在 article 应用目录下的 admin.py 文件中添加如下内容,就能够在网站后台管理页面看到 artcile 这个数据表模型。

#  DjangoBlog/DjangoBlog/article/admin.py
from django.contrib import admin

# 别忘了导入ArticlerPost
from .models import ArticlePost

# 注册ArticlePost到admin中
admin.site.register(ArticlePost)

刷新页面,就能够看到最新注册的 ArticlePost 数据表模型。

image-20240724220539053

点击页面上的 Add,就能够新增一条表记录,如下所示。

image-20240724220719156

保存之后,就能够看到保存的一条数据记录。

image-20240724220750774

1.6 artcile 文章应用视图

Django 中通过视图(View)进行后端数据的处理并向前端返回数据。

编写用于展示文章列表的 article_list 视图函数,在 article/views.py 中编写如下内容。

# DjangoBlog/DjangoBlog/article/views.py
from django.shortcuts import render

# 导入数据模型ArticlePost
from .models import ArticlePost

def article_list(request):
    # 取出所有博客文章
    articles = ArticlePost.objects.all()
    # 需要传递给模板(templates)的对象
    context = { 'articles': articles }
    # render函数:载入模板,并返回context对象
    return render(request, 'article/list.html', context)

如上所示,需要准备 article/list.html 文件,内容如下所示。

{% for article in articles %}
	<p>{{ article.title }}</p>
{% endfor %}

将模板文件统一放置到 templates 目录下进行管理,项目目录结构如下所示:

DjangoBlog
  db.sqlite3
  manage.py

├─article
  admin.py
  apps.py
  models.py
  tests.py
  views.py
  __init__.py

  └─migrations
        └─ __init__.py

├─DjangoBlog
  settings.py
  urls.py
  wsgi.py
   └─ __init__.py
└─templates
    └─article
          └─ list.html

修改 artcile 下的 urls.py 文件,添加 article_list 对应的路由地址,内容如下所示。

# 引入path
from django.urls import path
from . import views


# 正在部署的应用的名称
app_name = 'article'

urlpatterns = [
    # path函数将url映射到视图
    path('list/', views.article_list, name='article_list'),
]

浏览器中输入 http://127.0.0.1:8000/article/list/,就能够看到文章的列表。

image-20240724221817159

1.7 Bootstrap 美化博客页面

Bootstrap 是用于网站开发的开源前端框架,提供排版、字体、按钮等各种组件。

1.7.1 引入 Bootstrap 框架

templates 模板目录下新建一个 base.html 用来编写博客的主页面以及相关依赖,项目的当前目录结构为。

DjangoBlog
  db.sqlite3
  manage.py

├─article
  admin.py
  apps.py
  models.py
  tests.py
  views.py
  __init__.py

  └─migrations
        └─ __init__.py

├─DjangoBlog
  settings.py
  urls.py
  wsgi.py
   └─ __init__.py
└─templates
  article
     └─ list.html
    └─ base.html

base.html 文件的内容如下。

<!-- DjangoBlog/DjangoBlog/templates/base.html -->
{% load static %}

<!DOCTYPE html>
<!-- 网站主语言 -->
<html lang="zh-cn">

<head>
    <!-- 网站采用的字符编码 -->
    <meta charset="utf-8">
    <!-- 预留网站标题的位置 -->
    <title>{% block title %}{% endblock %}</title>
    <!-- 引入bootstrap的css文件 -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>

<body>
    <!-- 引入导航栏 -->
    {% include 'header.html' %}
    <!-- 预留具体页面的位置 -->
    {% block content %}{% endblock content %}
    <!-- 引入注脚 -->
    {% include 'footer.html' %}
    <!-- bootstrap.js 依赖 jquery.js 和popper.js,因此在这里引入 -->
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    
    <!-- 引入popper的js文件 -->
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1-lts/dist/umd/popper.min.js"></script>
   
    <!-- 引入bootstrap的js文件 -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</body>

</html>

1.7.2 Bootstrap 页面美化

templates 模板目录下新建一个 header.html 用来编写博客的导航栏菜单。

<!-- header.html -->
<!-- 定义导航栏 -->
<nav class="navbar navbar-expand-lg navbar-success bg-success">
    <div class="container">
  
      <!-- 导航栏商标 -->
      <a class="navbar-brand text-white" href="#">南歌EuanSu的个人网站</a>
  
      <!-- 导航入口 -->
      <div>
        <ul class="navbar-nav">
          <!-- 条目 -->
          <li class="nav-item">
            <a class="nav-link text-white" href="#">文章</a>
          </li>
        </ul>
      </div>
  
    </div>
  </nav>

templates 模板目录下新建一个 footer.html 用来编写博客底部的注脚。

<!-- footer.html -->
{% load static %}
<!-- Footer -->
<div>
    <br><br><br>
</div>
<footer class="py-3 bg-success fixed-bottom">
    <div class="container">
        <p class="m-0 text-center text-white">Copyright &copy; blog.euansu.cn 2024</p>
    </div>
</footer>

修改 artcile/list.html 文件。

<!-- extends表明此页面继承自 base.html 文件 -->
{% extends "base.html" %}
{% load static %}

<!-- 写入 base.html 中定义的 title -->
{% block title %}
    首页
{% endblock title %}

<!-- 写入 base.html 中定义的 content -->
{% block content %}

<!-- 定义放置文章标题的div容器 -->
<div class="container">
    <div class="row mt-2">

        {% for article in articles %}
        <!-- 文章内容 -->
        <div class="col-4 mb-4">
        <!-- 卡片容器 -->
            <div class="card h-100">
                <!-- 标题 -->
                <h4 class="card-header">{{ article.title }}</h4>
                <!-- 摘要 -->
                <div class="card-body">
                    <p class="card-text">{{ article.body|slice:'100' }}...</p>
                </div>
                <!-- 注脚 -->
                <div class="card-footer">
                    <a href="#" class="btn btn-success">阅读本文</a>
                </div>
            </div>
        </div>
        {% endfor %}

    </div>
</div>
{% endblock content %}

新增以及修改模板文件后,当前项目的目录结构为:

DjangoBlog
  db.sqlite3
  manage.py

├─article
  admin.py
  apps.py
  models.py
  tests.py
  views.py
  __init__.py

  └─migrations
        └─ __init__.py

├─DjangoBlog
  settings.py
  urls.py
  wsgi.py
   └─ __init__.py
└─templates
  article
     └─ list.html
  base.html
  header.html
    └─ footer.html

修改完成后,再次访问页面,就变成了如下内容。

image-20240724231404484

1.8 artcile 文章详情页面

artcile/views.py 中添加文章详情的视图函数,内容如下。

# 文章详情
def article_detail(request, id):
    # 取出相应的文章
    article = ArticlePost.objects.get(id=id)
    # 需要传递给模板的对象
    context = { 'article': article }
    # 载入模板,并返回context对象
    return render(request, 'article/detail.html', context)

templates/artcile 下添加 detail.html,添加后,当前项目的目录结构为:

DjangoBlog
  db.sqlite3
  manage.py

├─article
  admin.py
  apps.py
  models.py
  tests.py
  views.py
  __init__.py

  └─migrations
        └─ __init__.py

├─DjangoBlog
  settings.py
  urls.py
  wsgi.py
   └─ __init__.py
└─templates
  article
  detail.html
     └─ list.html
  base.html
  header.html
    └─ footer.html

detail.html模板文件的完整内容如下所示。

<!-- extends表明此页面继承自 base.html 文件 -->
{% extends "base.html" %}
{% load static %}

<!-- 写入 base.html 中定义的 title -->
{% block title %}
    文章详情
{% endblock title %}

<!-- 写入 base.html 中定义的 content -->
{% block content %}

<!-- 文章详情 -->
<div class="container">
    <div class="row">
        <!-- 标题及作者 -->
        <h1 class="col-12 mt-4 mb-4">{{ article.title }}</h1>
        <div class="col-12 alert alert-success">作者:{{ article.author }}</div>
        <!-- 文章正文 -->
        <div class="col-12">
            <p>{{ article.body }}</p>
        </div>
    </div>
</div>

{% endblock content %}

修改 artcile/urls.py,添加文章详情的路由函数,内容如下。

# 引入path
from django.urls import path
from . import views

urlpatterns = [
    ...
    # 文章详情
    path('detail/<int:id>/', views.article_detail, name='article_detail'),
]

修改 templates/artcile/list.html,页面上展示的 阅读原文 的链接,使其指向 detail/文章id 这个路由地址。

...
<a href="{% url 'article:article_detail' article.id %}" class="btn btn-success">阅读本文</a>
...

修改后的 templates/artcile/list.html 完整内容如下所示。

<!-- extends表明此页面继承自 base.html 文件 -->
{% extends "base.html" %}
{% load static %}

<!-- 写入 base.html 中定义的 title -->
{% block title %}
    首页
{% endblock title %}

<!-- 写入 base.html 中定义的 content -->
{% block content %}

<!-- 定义放置文章标题的div容器 -->
<div class="container">
    <div class="row mt-2">

        {% for article in articles %}
        <!-- 文章内容 -->
        <div class="col-4 mb-4">
        <!-- 卡片容器 -->
            <div class="card h-100">
                <!-- 标题 -->
                <h4 class="card-header">{{ article.title }}</h4>
                <!-- 摘要 -->
                <div class="card-body">
                    <p class="card-text">{{ article.body|slice:'100' }}...</p>
                </div>
                <!-- 注脚 -->
                <div class="card-footer">
                    <!-- <a href="#" class="btn btn-primary">阅读本文</a> -->
                    <a href="{% url 'article:article_detail' article.id %}" class="btn btn-success">阅读本文</a>
                </div>
            </div>
        </div>
        {% endfor %}

    </div>
</div>
{% endblock content %}

修改 templates/header.html,给文章这里添加链接,指向 list 文章列表这个接口。

<a class="nav-link text-white" href="{% url 'article:article_list' %}">文章</a>

修改后的 templates/header.html 完整内容如下所示。

<!-- 定义导航栏 -->
<nav class="navbar navbar-expand-lg navbar-success bg-success">
    <div class="container">
  
      <!-- 导航栏商标 -->
      <a class="navbar-brand text-white" href="#">南歌EuanSu的个人网站</a>
  
      <!-- 导航入口 -->
      <div>
        <ul class="navbar-nav">
          <!-- 条目 -->
          <li class="nav-item">
            <!-- <a class="nav-link text-white" href="#">文章</a> -->
            <a class="nav-link text-white" href="{% url 'article:article_list' %}">文章</a>
          </li>
        </ul>
      </div>
  
    </div>
  </nav>

再次访问 http://127.0.0.1:8000/article/list/ 页面,就能够看到文章详情的展示,并能够跳转文章列表。

点击阅读原文,跳转进入文章详情页。

image-20240724232759185

点击导航栏的文章,跳转回文章列表页面。

image-20240724232910964

1.9 Markdown 语法支持

Python 提供了markdown 第三方库来支持 Markdown 语法,激活虚拟环境后,使用如下命令。

pip install django -i https://pypi.tuna.tsinghua.edu.cn/simple

修改 artcile/views.py 中文章详情的视图函数,导入 markdown 后,将 markdown 语法渲染成 html,修改后的内容如下所示。

# DjangoBlog/DjangoBlog/article/views.py

# 引入markdown模块
import markdown

def article_detail(request, id):
    article = ArticlePost.objects.get(id=id)

    # 将markdown语法渲染成html样式
    article.body = markdown.markdown(article.body,
        extensions=[
        # 包含 缩写、表格等常用扩展
        'markdown.extensions.extra',
        # 语法高亮扩展
        'markdown.extensions.codehilite',
        ])

    context = { 'article': article }
    return render(request, 'article/detail.html', context)

修改 templates/article/detail.html 的正文展示内容,需要添加 safe 过滤器,修改后的 detail.html 完整内容如下所示。

<!-- extends表明此页面继承自 base.html 文件 -->
{% extends "base.html" %}
{% load static %}

<!-- 写入 base.html 中定义的 title -->
{% block title %}
    文章详情
{% endblock title %}

<!-- 写入 base.html 中定义的 content -->
{% block content %}

<!-- 文章详情 -->
<div class="container">
    <div class="row">
        <!-- 标题及作者 -->
        <h1 class="col-12 mt-4 mb-4">{{ article.title }}</h1>
        <div class="col-12 alert alert-success">作者:{{ article.author }}</div>
        <!-- 文章正文 -->
        <div class="col-12">
            <!-- <p>{{ article.body }}</p> -->
            <p>{{ article.body|safe }}</p>
        </div>
    </div>
</div>

{% endblock content %}

Django 出于安全的考虑,会将输出的 HTML 代码进行转义,这使得 article.body中渲染的 HTML 文本无法正常显示,管道符|Django 中过滤器的写法,而|safe就类似给article.body贴了一个标签,表示这一段字符不需要进行转义了。

修改完成后,在后台新增一篇博文,内容如下。

# 国风·周南·关雎
---
**关关雎鸠,在河之洲。窈窕淑女,君子好逑。**

参差荇菜,左右流之。窈窕淑女,寤寐求之。

---
+ 列表一
+ 列表二
    + 列表二-1
    + 列表二-2
---

```python
def article_detail(request, id):
	article = ArticlePost.objects.get(id=id)
	# 将markdown语法渲染成html样式
	article.body = markdown.markdown(article.body,
		extensions=[
		# 包含 缩写、表格等常用扩展
		'markdown.extensions.extra',
		# 语法高亮扩展
		'markdown.extensions.codehilite',
		])
	context = { 'article': article }
	return render(request, 'article/detail.html', context)
```

新增博文后访问页面,即可看到如下内容,能够正确处理 Markdown 语法。

image-20240725100250195

上面的文章中,新增了 python 代码块的展示,但是并没有代码高亮,这里需要继续修改。

Pygments 是一种通用语法高亮显示器,可以帮助我们自动生成美化代码的样式文件,使用如下命令即可安装。

pip install Pygments -i https://pypi.tuna.tsinghua.edu.cn/simple

安装完 Pygments 后,新建 static/css 静态文件目录,用于放置代码高亮的样式文件,在命令行中进入新建的 css 目录中,输入 Pygments 指令,如下所示。

pygmentize -S monokai -f html -a .codehilite > monokai.css

image-20240725101025327

执行完成后,会在目录下生成一个名为 monokai.css 的样式文件。

image-20240725101159097

settings.py 文件配置静态资源,内容如下。

# 静态文件 URL 前缀
STATIC_URL = '/static/'

# 额外的静态文件目录
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static'),
]

配置完成后,需要在 base.html中引入 monikai.css,这里我们也自定义了样式一并进行引入,引入完成后的 base.html 完整内容如下。

{% load static %}

<!DOCTYPE html>
<!-- 网站主语言 -->
<html lang="zh-cn">

<head>
    <!-- 网站采用的字符编码 -->
    <meta charset="utf-8">
    <!-- 预留网站标题的位置 -->
    <title>{% block title %}{% endblock %}</title>
    <!-- 引入bootstrap的css文件 -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
    <!-- 引入monikai.css -->
    <link rel="stylesheet" href="{% static 'css/monokai.css' %}">
    <!-- 引入custom.css -->
    <link rel="stylesheet" href="{% static 'css/custom.css' %}">
</head>

<body>
    <!-- 引入导航栏 -->
    {% include 'header.html' %}
    <!-- 预留具体页面的位置 -->
    {% block content %}{% endblock content %}
    <!-- 引入注脚 -->
    {% include 'footer.html' %}
    <!-- bootstrap.js 依赖 jquery.js 和popper.js,因此在这里引入 -->
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    
    <!-- 引入popper的js文件 -->
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1-lts/dist/umd/popper.min.js"></script>
   
    <!-- 引入bootstrap的js文件 -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</body>

</html>

自定义的样式文件 custom.css 的完整内容如下,custom.css 文件放置到 static/css 目录下。

.bg-custom {  
    background-color: #883B89; /* 例如,使用Bootstrap的绿色 */  
}  

.navbar-custom .navbar-brand,  
.navbar-custom .nav-link {  
    color: white; /* 字体颜色设置为白色 */  
}  

.btn-custom {
    background-color: #883B89; /* 例如,使用Bootstrap的绿色 */  
    color: white; /* 字体颜色设置为白色 */  
}

再次访问页面,能够看到代码高亮以及底边的颜色发生了变化。

image-20240725103619017

1.10 artcile 文章发表页面

新增一个 artcileform 表单,用来创建新文章。

# 引入表单类
from django import forms
# 引入文章模型
from .models import ArticlePost

# 写文章的表单类
class ArticlePostForm(forms.ModelForm):
    class Meta:
        # 指明数据模型来源
        model = ArticlePost
        # 定义表单包含的字段,这里的字段需要
        fields = ('title', 'body')

增加一个文章发表的后端函数。

def article_create(request):
    # 判断用户是否提交数据
    if request.method == "POST":
        # 将提交的数据赋值到表单实例中
        article_post_form = ArticlePostForm(data=request.POST)
        # 判断提交的数据是否满足模型的要求
        if article_post_form.is_valid():
            # 保存数据,但暂时不提交到数据库中
            new_article = article_post_form.save(commit=False)
            # 指定数据库中 id=1 的用户为作者
            new_article.author = User.objects.get(id=1)
            # 将新文章保存到数据库中
            new_article.save()
            # 完成后返回到文章列表
            return redirect("article:article_list")
        # 如果数据不合法,返回错误信息
        else:
            # 创建表单类实例
            article_post_form = ArticlePostForm()
            # 赋值上下文
            context = { 'article_post_form': article_post_form , 'msg':'表单填写有误,请重新填写后提交。'}
            # 返回模板
            return render(request, 'article/create.html', context)
    # 如果用户请求获取数据
    else:
        # 创建表单类实例
        article_post_form = ArticlePostForm()
        # 赋值上下文
        context = { 'article_post_form': article_post_form }
        # 返回模板
        return render(request, 'article/create.html', context)

文件发表的 article/create.html 内容如下。

<!-- extends表明此页面继承自 base.html 文件 -->
{% extends "base.html" %} {% load static %}
<!-- 写入 base.html 中定义的 title -->
{% block title %} 写文章 {% endblock title %}
<!-- 写入 base.html 中定义的 content -->
{% block content %}

<!-- 失败告警 -->
{% if msg %}
<div class="container">
    <div class="row">
        <div class="col-6">
            <div class="alert alert-danger alert-dismissible fade show my-3" role="alert">
                {{msg}}
                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
            </div>  
        </div>
    </div>
</div>
{% endif %}


<!-- 写文章表单 -->
<div class="container">
    <div class="row">
        <div class="col-12">
            <br>
            <!-- 提交文章的表单 -->
            <form method="post" action=".">
                <!-- Django中需要POST数据的地方都必须有csrf_token -->
                {% csrf_token %}
                <!-- 文章标题 -->
                <div class="form-group">
                    <!-- 标签 -->
                    <label for="title">文章标题</label>
                    <!-- 文本框 -->
                    <input type="text" class="form-control" id="title" name="title">
                </div>
                <!-- 文章正文 -->
                <div class="form-group">
                    <label for="body">文章正文</label>
                    <!-- 文本区域 -->
                    <textarea type="text" class="form-control" id="body" name="body" rows="12"></textarea>
                </div>
                <!-- 提交按钮 -->
                <button type="submit" class="btn btn-primary my-3">提交</button>
            </form>
        </div>
    </div>
</div>
{% endblock content %}

artcile/urls.py 新增文章发表的路由入口,如下所示。

path('create/', views.article_create, name='article_create'),

此外,还需要给文章发表做一个入口,将文章发表的入口放置在导航栏的右上方,修改 header.html,修改成如下内容。

<!-- 定义导航栏 -->
<!-- <nav class="navbar navbar-expand-lg navbar-success bg-success"> -->
<nav class="navbar navbar-expand-lg navbar-custom bg-custom">
    <div class="container">
  
      <!-- 导航栏商标 -->
      <a class="navbar-brand text-white" href="#">南歌EuanSu的个人网站</a>
  
      <!-- 导航入口 -->
      <div>
        <ul class="navbar-nav">
          <!-- 条目 -->
          <li class="nav-item">
            <!-- <a class="nav-link text-white" href="#">文章</a> -->
            <a class="nav-link text-white" href="{% url 'article:article_create' %}">新增文章</a>
          </li>
          <li class="nav-item">
            <!-- <a class="nav-link text-white" href="#">文章</a> -->
            <a class="nav-link text-white" href="{% url 'article:article_list' %}">文章</a>
          </li>
        </ul>
      </div>
    </div>
  </nav>

实现的效果如下,导航栏右上方出现【新增文章】的字样,点击可以进行文章发表页面。

image-20240801181320169

文章发表页面如下。

image-20240801181419790

什么内容也不填写,直接提交,会有报错。

image-20240801181512842

填写标题和正文后,就能够正常发表文章,点击提交后,跳转至文章列表页。

image-20240801181745712

点击阅读本文,能够正常查看提交的文章内容。

image-20240801181859874

1.11 artcile 文章删除

修改 ArticlePost 模型,新增一个 is_deleted 属性,标记文章是否被删除。

# 标记文章是否被删除
is_deleted = models.BooleanField(default=False)

新增如上属性后,执行文章的迁移操作,如下所示。

# 生成迁移文件
python manage.py makemigrations
# 应用迁移到数据库
python manage.py migrate

image-20240801182441436

接下来编写文章删除的视图函数 article_delete ,如下所示,当请求该接口时,进行文章的删除,删除成功后,跳转至文章列表页面。

# article/views.py
def article_delete(request, id):
    # 根据 id 获取需要删除的文章
    article = ArticlePost.objects.get(id=id)
    # 标记文章为删除状态
    article.is_deleted = True
    article.save()
    # 完成删除后返回文章列表
    return redirect("article:article_list")

编辑 urls.py 路由文件,添加文章删除的路由函数,如下所示。

# article/urls.py
path('delete/<int:id>/', views.article_delete, name='article_delete'),

在文章详情页``detail.html` 增加删除的入口。

<div class="col-12 alert alert-success">作者:{{ article.author }}
    · <a href="#" onclick="confirm_delete()">删除文章</a>
</div>

image-20240801183426928

引入一个弹窗的插件库 sweetalert2,官方地址是 https://sweetalert2.github.io/,在 base.html 也即我们设置的基础页面中引入这个插件,如下所示。

<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>

修改 detail.html文件,增加删除文章的弹窗以及后端请求,修改后的文件如下所示。

<!-- extends表明此页面继承自 base.html 文件 -->
{% extends "base.html" %}
{% load static %}

<!-- 写入 base.html 中定义的 title -->
{% block title %}
文章详情
{% endblock title %}

<!-- 写入 base.html 中定义的 content -->
{% block content %}

<!-- 文章详情 -->
<div class="container">
    <div class="row">
        <!-- 标题及作者 -->
        <h1 class="col-12 mt-4 mb-4">{{ article.title }}</h1>
        <!-- <div class="col-12 alert alert-success">作者:{{ article.author }}</div> -->
        <div class="col-12 alert alert-success">作者:{{ article.author }}
            · <a href="#" onclick="confirm_delete()">删除文章</a>
            <!-- · <a href=" {% url "article:article_delete" article.id %}">删除文章</a> -->
        </div>
        <!-- <div class="p-3 mb-2 bg-info-subtle text-info-emphasis">作者:{{ article.author }}</div> -->
        <!-- 文章正文 -->
        <div class="col-12">
            <!-- <p>{{ article.body }}</p> -->
            <p class="font-monospace">{{ article.body|safe }}</p>
        </div>
    </div>
</div>

<script>
    // sweetalert2组件库
    function confirm_delete() {
        Swal.fire({
            title: "你确定要删除这篇文章吗?",
            text: "文章删除后无法撤销!",
            icon: "warning",
            showCancelButton: true,
            confirmButtonColor: "#3085d6",
            cancelButtonColor: "#d33",
            confirmButtonText: "确认",
            cancelButtonText: "取消"
        }).then((result) => {
            if (result.isConfirmed) {
                window.location.href = "{% url 'article:article_delete' article.id %}";
            }
        });
    }
</script>

页面上点击 删除文章,弹出 sweetalert2 的对话框。

image-20240801231456545

在弹窗中选择确认,完成后跳转至文章列表页面,文章被删除。

image-20240801231535956

1.12 CSRF 令牌

Django 中提交表单必须添加 csrf_token,也即 CSRF 令牌,用于防范 CSRF 攻击流程,具体的过程如下:

  • 用户访问 django 站点时,django 反馈给用户的表单中有一个隐含字段 csrf_token,这个值是在服务器端随机生成的,每次生成的值都不一样。
  • 在后端处理 POST 请求前,django 会校验请求 cookie 里的 csrf_token 和表单里的 csrf_token 是否一致,如果是一致的则说明是合法请求,否则这个请求则可能是来自于 CSRF 工具,返回 403 服务器禁止访问。

在这一过程中,攻击者无法获取到用户的 cookie 内容(仅依靠浏览器做转发),因此通常情况是无法构造出正确的 csrf_token,从而防范了 CSRF 攻击。

1.11 章节就写了一个请求文章删除的操作,这一操作通常是不可逆的,这里我们做修改,让其携带 csrf_token 做请求,我们对代码做如下改造。

首先是修改前端页面,在删除文章的操作下新增一个表单,将文章的删除操作由该页面发起。

<!-- detail.html -->
· <a href="#" onclick="confirm_delete()">删除文章</a>
<!-- 新增一个隐藏的表单 -->
<form style="display:none;" id="safe_delete" action="{% url 'article:article_delete' article.id %}"
      method="POST">
    {% csrf_token %}
    <button type="submit">发送</button>
</form>

同时修改 sweetalert2 的确认结果,调用这个表单的 submit 方法,如下所示。

<script>
    // sweetalert2组件库
    function confirm_delete() {
        Swal.fire({
            title: "你确定要删除这篇文章吗?",
            text: "文章删除后无法撤销!",
            icon: "warning",
            showCancelButton: true,
            confirmButtonColor: "#3085d6",
            cancelButtonColor: "#d33",
            confirmButtonText: "确认",
            cancelButtonText: "取消"
        }).then((result) => {
            if (result.isConfirmed) {
                // window.location.href = "{% url 'article:article_delete' article.id %}";
                // 这里调用表单的提交方法
                $('form#safe_delete button').click();
            }
        });
    }
</script>

修改后端的视图函数,限制为仅允许 POST 操作,也即必须携带 csrf_token 这样就能够保证发送的请求中一定携带了 csrf_token

# article/views.py
def article_delete(request, id):
    # 安全删除文章
    if request.method == 'POST':
        article = ArticlePost.objects.get(id=id)
        article.is_deleted = True
        article.save()
        return redirect("article:article_list")
    else:
        return HttpResponse("仅允许post请求")

说明:csrf_token 的校验并不用再次实现,django 默认的中间件 django.middleware.csrf.CsrfViewMiddleware 已经实现了 csrf_token 的校验,并不需要我们在额外做其他操作,只需要保证请求 django 服务器的时候携带 csrf 令牌即可。

1.12 artcile 文章修改页面

如上已经实现了文章的 发表删除展示,接下来就是要对发表的文章做修改,修改的时候需要默认填充文章内容至页面中。

首先编写 artcile_update 视图函数,实现文章的更新操作。

# artcile/views.py
def article_update(request, id):
    # 获取所需要具体修改的文章对象
    article = ArticlePost.objects.get(id=id)
    if request.method == 'POST':
        # 将提交的数据赋值到表单实例中
        article_post_form = ArticlePostForm(data=request.POST)
        # 判断提交的数据是否满足模型的要求
        if article_post_form.is_valid():
            # 保存新写入的 title、body 数据并保存
            article.title = request.POST['title']
            article.body = request.POST['body']
            article.save()
            # 完成后返回到修改后的文章中。需传入文章的 id 值
            return redirect("article:article_detail", id=id)
        # 如果数据不合法,返回错误信息
        else:
            # 创建表单类实例
            article_post_form = ArticlePostForm()
            # 赋值上下文,将 article 文章对象也传递进去,以便提取旧的内容
            context = { 'article': article, 'article_post_form': article_post_form , 'msg':"填写的文章内容有误,请重新填写!"}
            # 将响应返回到模板中
            return render(request, 'article/update.html', context)
    else:
        # 创建表单类实例
        article_post_form = ArticlePostForm()
        # 赋值上下文,将 article 文章对象也传递进去,以便提取旧的内容
        context = { 'article': article, 'article_post_form': article_post_form }
        # 将响应返回到模板中
        return render(request, 'article/update.html', context)

urls.py 中添加对应的路由函数。

path('update/<int:id>/', views.article_update, name='article_update'),

新增 article/update.html 文章更新页面,这里的页面与 create.html 基本一致,知识额外添加了标题和内容的展示。

<!-- extends表明此页面继承自 base.html 文件 -->
{% extends "base.html" %} {% load static %}
<!-- 写入 base.html 中定义的 title -->
{% block title %} 写文章 {% endblock title %}
<!-- 写入 base.html 中定义的 content -->
{% block content %}

<!-- 失败告警 -->
{% if msg %}
<div class="container">
    <div class="row">
        <div class="col-6">
            <div class="alert alert-danger alert-dismissible fade show my-3" role="alert">
                {{msg}}
                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
            </div>  
        </div>
    </div>
</div>
{% endif %}


<!-- 写文章表单 -->
<div class="container">
    <div class="row">
        <div class="col-12">
            <br>
            <!-- 提交文章的表单 -->
            <form method="post" action=".">
                <!-- Django中需要POST数据的地方都必须有csrf_token -->
                {% csrf_token %}
                <!-- 文章标题 -->
                <div class="form-group">
                    <!-- 标签 -->
                    <label for="title">文章标题</label>
                    <!-- 文本框 -->
                    <!-- 这里添加上了文章的标题展示 -->
                    <input type="text" class="form-control" id="title" name="title" value="{{article.title}}">
                </div>
                <!-- 文章正文 -->
                <div class="form-group">
                    <label for="body">文章正文</label>
                    <!-- 文本区域 -->
                    <!-- 这里添加上了文章的内容展示 -->
                    <textarea type="text" class="form-control" id="body" name="body" rows="12">{{ article.body }}</textarea>
                </div>
                <!-- 提交按钮 -->
                <button type="submit" class="btn btn-primary my-3">提交</button>
            </form>
        </div>
    </div>
</div>
{% endblock content %}

修改页面详情的 html 文件,添加删除文章的入口。

<!-- detail.html -->
· <a href="#" onclick="confirm_delete()">删除文章</a>
· <a href=" {% url "article:article_update" article.id %}">修改文章</a>

接下来进行页面测试,点击修改文章,能够进入到文章修改页面。

image-20240801235327955

修改内容后,点击提交。

image-20240801235355044

提交到跳转至详情页,内容如下。

image-20240801235412638

1.13 artcile 文章分页

django 提供了分页的类 Paginator,直接导入即可使用,一个简单的使用示例如下。

image-20240805141201404

修改 artcile/views.py,首先是导入 Paginator,接下来则是对文章列表进行分页,分页后的数据及分页结果传输到前端页面。

# artcile/views.py
# 导入django自带的分页模块
from django.core.paginator import Paginator

def article_list(request):
    # 取出所有博客文章
    articles = ArticlePost.objects.filter(is_deleted=False)
    # 每页显示 1 篇文章
    paginator = Paginator(articles, 1)
    # 获取 url 中的页码
    page = request.GET.get('page')
    # 将导航对象相应的页码内容返回给 articles
    articles = paginator.get_page(page)
    # paginator.page_range 页码列表
    context = { 'articles': articles ,'page_list':paginator.page_range}
    return render(request, 'article/list.html', context)

修改 template/artcile/list.html 页面,添加分页器的展示,这里的分页器直接取自 bootstrap,也可以自己单独编写一个。

<!-- extends表明此页面继承自 base.html 文件 -->
{% extends "base.html" %}
{% load static %}

<!-- 写入 base.html 中定义的 title -->
{% block title %}
首页
{% endblock title %}

<!-- 写入 base.html 中定义的 content -->
{% block content %}

<!-- 定义放置文章标题的div容器 -->
<div class="container text-center">
    <div class="row">
        <div class="col-8">
            <ul class="list-group list-group-flush">
                {% for article in articles %}
                <li class="list-group-item">
                    <div class="container text-center">
                        <div class="row">
                            <!-- <p href="{% url 'article:article_detail' article.id %}" class="fs-2 font-monospace text-start fw-bold">{{ article.title }}</p> -->
                            <a href="{% url 'article:article_detail' article.id %}"
                                class="fs-3 font-monospace text-start fw-bold text-decoration-none text-dark">
                                {{ article.title }}
                            </a>

                        </div>
                        <div class="row">
                            <p class="fs-6 font-monospace text-start">{{ article.updated | date:"Y年n月j日"}}</p>
                        </div>
                        <div class="row">
                            <div class="d-grid gap-2 col-2 ms-auto">
                                <a href="{% url 'article:article_detail' article.id %}" class="btn btn-success">阅读本文</a>
                                <!-- <button class="btn btn-success" type="button">
                                    阅读原文
                                </button> -->
                            </div>
                        </div>
                    </div>
                </li>
                {% endfor %}
            </ul>

            <nav aria-label="Page navigation example">
                <ul class="pagination">
                    
                    <!-- 只有不是一页的时候,才需要展示前一页的按钮 -->
                    {% if articles.has_previous %}
                    <li class="page-item">
                        <a class="page-link" href="?page={{ articles.previous_page_number }}" aria-label="Previous">
                            <span aria-hidden="true">&laquo;</span>
                            <span class="sr-only">Previous</span>
                        </a>
                    </li>
                    {% endif %}

                    <!-- 页码展示 -->
                    {% for item in page_list %}
                    <li class="page-item"><a class="page-link" href="?page={{ item }}">{{item}}</a></li>
                    {% endfor %}

                    <!-- 只有不是最后一页的时候,才需要展示下一页的按钮 -->
                    {% if articles.has_next %}
                    <li class="page-item">
                        <a class="page-link" href="?page={{ articles.next_page_number }}" aria-label="Next">
                            <span aria-hidden="true">&raquo;</span>
                            <span class="sr-only">Next</span>
                        </a>
                    </li>
                    {% endif %}

                </ul>
            </nav>
        </div>
        <div class="col-4">
            头像及博客简介
        </div>
    </div>
</div>

{% endblock content %}

重启 django 服务器后,文章列表下方就出现了分页器,实现的效果如下。

分页实现

1.14 artcile 文章目录

目录也即在页面能够展示文章的标题结构,帮助我们快速定位感兴趣或重点内容,这里同样使用 markdown 自带的方式实现。

首先是修改文章详情的视图,这里引入 markdown.extensions.toc 插件,并将生成的目录内容传送至前端。

def article_detail(request, id):
    article = ArticlePost.objects.get(id=id)
    md = markdown.Markdown(
        extensions=[
        'markdown.extensions.extra',
        'markdown.extensions.codehilite',
        # 目录扩展 
        'markdown.extensions.toc',
        ]
    )
    article.body = md.convert(article.body)

    context = { 'article': article, 'toc':md.toc }
    return render(request, 'article/detail.html', context)

修改 detail.html 文章详情页面,添加目录的内容展示,如下所示。

<!-- detail.html -->
<!-- 文章正文 -->
<h4><strong>目录</strong></h4>
<hr>
<div>
    {{ toc|safe }}
</div>

修改后的页面展示如下。

image-20240806220616159

这样的效果还是有些不好,我们使用 bootstrap将目录移动至文章右侧展示,也即文章占用左侧,而目录占用右侧,如下所示。

 <div class="container">
     <div class="row">
         <!-- 将原有内容嵌套进新的div中 -->
         <div class="col-9">
             <p class="font-monospace">{{ article.body|safe }}</p>
         </div>

         <!-- 新增的目录 -->
         <div class="col-3 mt-4">
             <h4><strong>目录</strong></h4>
             <hr>
             <div>
                 {{ toc|safe }}
             </div>
         </div>
     </div>
</div>

修改后的目录展示如下。

image-20240806221559055

2.userprofile 用户管理应用

2.1 userprofile 用户应用创建及注册

激活虚拟环境后,执行如下命令创建 userprofile 用户管理应用。

python manage.py startapp userprofile

image-20240802081236370

settings.py 中的 INSTALLED_APPS 中添加 userprofile,如下所示。

INSTALLED_APPS = [
    ...
    'userprofile'
]

2.2 userprofile 用户的登录和注销

userprofile 目录下创建 forms.py 表单文件,编写如下代码。

# userprofile/forms.py
# 引入表单类
from django import forms
# 引入 User 模型
from django.contrib.auth.models import User

# 登录表单,继承了 forms.Form 类
# forms.Form则需要手动配置每个字段,它适用于不与数据库进行直接交互的功能。用户登录不需要对数据库进行任何改动,这里继承form.Form指定需要的字段即可
class UserLoginForm(forms.Form):
    username = forms.CharField()
    password = forms.CharField()

userprofile 目录下的 views.py 编写视图函数,如下所示,authenticate() 方法验证用户名称和密码是否匹配,如果是,则将这个用户数据返回。login() 方法实现用户登录,将用户数据保存在 session 中。

# userprofile/views.py
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login
from django.http import HttpResponse
from .forms import UserLoginForm

# Create your views here.

def user_login(request):
    if request.method == 'POST':
        user_login_form = UserLoginForm(data=request.POST)
        if user_login_form.is_valid():
            # .cleaned_data 清洗出合法数据
            data = user_login_form.cleaned_data
            # 检验账号、密码是否正确匹配数据库中的某个用户
            # 如果均匹配则返回这个 user 对象
            user = authenticate(username=data['username'], password=data['password'])
            if user:
                # 将用户数据保存在 session 中,即实现了登录动作
                login(request, user)
                return redirect("article:article_list")
            else:
                user_login_form = UserLoginForm()
                context = { 'form': user_login_form , 'msg':'账号或密码输入有误。请重新输入~'}
                return render(request, 'userprofile/login.html', context)
        else:
            user_login_form = UserLoginForm()
            context = { 'form': user_login_form, 'msg':'账号或密码输入不合法~'}
            return render(request, 'userprofile/login.html', context)
    elif request.method == 'GET':
        user_login_form = UserLoginForm()
        context = { 'form': user_login_form }
        return render(request, 'userprofile/login.html', context)
    else:
        return HttpResponse("请使用GET或POST请求数据")

userprofile 目录下创建 urls.py,编写如下路由函数。

# userprofile/urls.py
from django.urls import path
from . import views

app_name = 'userprofile'

urlpatterns = [
    # 用户登录
    path('login/', views.user_login, name='login'),
]

接下来需要在项目目录下的 urls.py 配置 userprofile 的路由函数,如下所示。

urlpatterns = [
    ...
    # 新增代码,配置userprofile的url
    path('userprofile/', include('userprofile.urls', namespace='userprofile')),
]

最后是表单页面,首先是编写登录页面,内容如下。

{% extends "base.html" %} {% load static %}
{% block title %} 登录 {% endblock title %}
{% block content %}

<!-- 失败告警 -->
{% if msg %}
<div class="container">
    <div class="row justify-content-center">
        <div class="col-4">
            <div class="alert alert-danger alert-dismissible fade show my-3" role="alert">
                {{msg}}
                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
            </div>  
        </div>
    </div>
</div>
{% endif %}


<div class="container">
    <div class="row justify-content-center">
        <div class="col-4">
            <br>
            <form method="post" action=".">
                {% csrf_token %}
                <!-- 账号 -->
                <div class="form-group">
                    <label for="username">账号</label>
                    <input type="text" class="form-control" id="username" name="username">
                </div>
                <!-- 密码 -->
                <div class="form-group">
                    <label for="password">密码</label>
                    <input type="password" class="form-control" id="password" name="password">
                </div>
                <!-- 提交按钮 -->
                <button type="submit" class="btn btn-primary my-3">登录</button>
            </form>
        </div>
    </div>
</div>

{% endblock content %}

修改 header.html 页面,这里添加登录的入口操作,修改后的 header.html 内容如下,主要是添加了登录入口,如果用户未登录则显示登录,否则显示登录后的用户名以及主要的入口。

<!-- 定义导航栏 -->
<!-- <nav class="navbar navbar-expand-lg navbar-success bg-success"> -->
<nav class="navbar navbar-expand-lg navbar-custom bg-custom">
  <div class="container">

    <!-- 导航栏商标 -->
    <a class="navbar-brand text-white" href="#">南歌EuanSu的个人网站</a>

    <!-- 导航入口 -->
    <div>
      <ul class="navbar-nav">
        <!-- 条目 -->
        <li class="nav-item">
          <!-- <a class="nav-link text-white" href="#">文章</a> -->
          <a class="nav-link text-white" href="{% url 'article:article_create' %}">新增文章</a>
        </li>
        <li class="nav-item">
          <!-- <a class="nav-link text-white" href="#">文章</a> -->
          <a class="nav-link text-white" href="{% url 'article:article_list' %}">文章</a>
        </li>
        <!-- Django的 if 模板语句 -->
        {% if user.is_authenticated %}
        <!-- 如果用户已经登录,则显示用户名下拉框 -->
        <li class="nav-item dropdown">
          <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown"
            aria-haspopup="true" aria-expanded="false">
            {{ user.username }}
          </a>
          <div class="dropdown-menu" aria-labelledby="navbarDropdown">
            <a class="dropdown-item" href="#">注销登录</a>
          </div>
        </li>
        <!-- 如果用户未登录,则显示 “登录” -->
        {% else %}
        <li class="nav-item">
          <a class="nav-link" href="{% url 'userprofile:login' %}">登录</a>
        </li>
        <!-- if 语句在这里结束 -->
        {% endif %}
      </ul>
    </div>

  </div>
</nav>

实现的效果如下,输入的错误的密码。

image-20240804213558802

未输入账号密码进行登录。

image-20240804213630167

输入正确的账号密码,跳转至文章列表页面。

image-20240804213657143

接下来则是实现注销登录的操作,如上已经实现了对应的入口,这里只需要编写路由函数和视图函数即可,同时需要在前端点击注销的内容请求 diango 的对应函数。

userprofile/views.py 中编写注销登录的函数,如下,这里依旧是直接调用 django 提供的 logout 方法。

from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login, logout

# 注销登录
def user_logout(request):
    logout(request)
    return redirect("userprofile:login")

userprofile/urls.py 中添加对应的路由函数。

urlpatterns = [
    ...
    # 注销登录
    path('logout/', views.user_logout, name='logout'),
]

将页面上注销登录的位置,编写对应请求调用,也即在 header.html 中修改 注销登录 部分的内容,如下所示。

<li><a class="dropdown-item" href='{% url "userprofile:logout" %}'>注销登录</a></li>

最终实现的效果如下所示。

注销登录

2.3 userprofile 用户的注册

用户注册这里也使用 django 提供的表单来实现,对于数据库的操作需要继承 forms.ModelForm,能够自动生成模型中已有的字段,对于需要修改的字段,直接编写对应的字段即可。

# userprofile/forms.py
class UserRegisterForm(forms.ModelForm):
    # 复写 User 的密码
    password = forms.CharField()
    password2 = forms.CharField()

    class Meta:
        model = User
        fields = ('username', 'email')

    # 对两次输入的密码是否一致进行检查
    def clean_password2(self):
        data = self.cleaned_data
        if data.get('password') == data.get('password2'):
            return data.get('password')
        else:
            raise forms.ValidationError("密码输入不一致,请重试。")

接下来则是在 userprofile/views.py 中添加用户注册的视图函数,如下所示。

# userprofile/views.py
def user_register(request):
    if request.method == 'POST':
        user_register_form = UserRegisterForm(data=request.POST)
        if user_register_form.is_valid():
            new_user = user_register_form.save(commit=False)
            # 设置密码
            new_user.set_password(user_register_form.cleaned_data['password'])
            new_user.save()
            # 保存好数据跳转至登录页
            return redirect("userprofile:login")
        else:
            user_register_form = UserRegisterForm()
            context = { 'form': user_register_form , 'msg':"请按照格式填写用户注册表单~"}
            return render(request, 'userprofile/register.html', context)
    elif request.method == 'GET':
        user_register_form = UserRegisterForm()
        context = { 'form': user_register_form }
        return render(request, 'userprofile/register.html', context)
    else:
        return HttpResponse("请使用GET或POST请求数据")

编写注册的页面,在 template 下新增一个 register.html,如下所示。

{% extends "base.html" %} {% load static %}
{% block title %} 登录 {% endblock title %}
{% block content %}

<!-- 失败告警 -->
{% if msg %}
<div class="container">
    <div class="row justify-content-center">
        <div class="col-4">
            <div class="alert alert-danger alert-dismissible fade show my-3" role="alert">
                {{msg}}
                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
            </div>
        </div>
    </div>
</div>
{% endif %}

<div class="container">
    <div class="row justify-content-center">
        <div class="col-4">
            <br>
            <form method="post" action=".">
                {% csrf_token %}
                <!-- 账号 -->
                <div class="form-group col-md-12">
                    <label for="username">昵称</label>
                    <input type="text" class="form-control" id="username" name="username" required>
                </div>
                <!-- 邮箱 -->
                <div class="form-group col-md-12">
                    <label for="email">Email</label>
                    <input type="text" class="form-control" id="email" name="email">
                </div>
                <!-- 密码 -->
                <div class="form-group col-md-12">
                    <label for="password">设置密码</label>
                    <input type="password" class="form-control" id="password" name="password" required>
                </div>
                <!-- 确认密码 -->
                <div class="form-group col-md-12">
                    <label for="password2">确认密码</label>
                    <input type="password" class="form-control" id="password2" name="password2" required>
                </div>
                <!-- 提交按钮 -->
                <div class="d-grid col-12">
                    <button type="submit" class="btn btn-primary my-2">注册</button>
                </div>
            </form>
        </div>
    </div>
</div>
{% endblock content %}

页面显示的效果如下。

image-20240805121928835

修改登录页 template/userprofile/login.html,添加注册的入口,修改后的 template/userprofile/login.html 如下所示。

{% extends "base.html" %} {% load static %}
{% block title %} 登录 {% endblock title %}
{% block content %}

<!-- 失败告警 -->
{% if msg %}
<div class="container">
    <div class="row justify-content-center">
        <div class="col-4">
            <div class="alert alert-danger alert-dismissible fade show my-3" role="alert">
                {{msg}}
                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
            </div>
        </div>
    </div>
</div>
{% endif %}


<div class="container">
    <div class="row justify-content-center">
        <div class="col-4">
            <br>
            <form method="post" action=".">
                {% csrf_token %}
                <!-- 账号 -->
                <div class="form-group">
                    <label for="username">账号</label>
                    <input type="text" class="form-control" id="username" name="username">
                </div>
                <!-- 密码 -->
                <div class="form-group">
                    <label for="password">密码</label>
                    <input type="password" class="form-control" id="password" name="password">
                </div>
                <div class="row">
                    <div class="col-4 ms-auto my-1">
                        <a href='{% url "userprofile:register" %}' class="text-dark">注册账号</a>
                    </div>
                </div>

                <!-- <br>
                <h5>还没有账号?</h5>
                <h5>点击<a href='{% url "userprofile:register" %}'>注册账号</a></h5>
                <br> -->
                <!-- 提交按钮 -->
                <div class="d-grid col-12">
                    <button type="submit" class="btn btn-primary my-1">登录</button>
                </div>
            </form>
        </div>
    </div>
</div>

{% endblock content %}

修改后的登录页面效果如下。

image-20240805122605066

实际测试效果如下。

注册

2.4 userprofile 重置用户密码

pip install django-password-reset -i https://pypi.tuna.tsinghua.edu.cn/simple

2.5 userprofile 用户信息拓展

说明:这里 Django扩展用户信息的时候有报错,需要清空数据库重新迁移。

首先是修改 userprofile/models.py 模型文件,继承 AbstractUser 用户,额外添加电话、头像、简介字段,如下所示。

# userprofile/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser

# 继承用户信息
class UserInfo(AbstractUser):
    # 电话号码字段
    phone = models.CharField(max_length=20, blank=True)
    # 头像
    avatar = models.ImageField(upload_to='avatar/%Y%m%d/', blank=True)
    # 个人简介
    bio = models.TextField(max_length=500, blank=True)

在项目的 settings.py 文件中添加 AUTH_USER_MODEL = 'userprofile.UserInfo',这里的 userprofile 是应用的名称,UserInfo 则是继承 User 的模型类。

此外,新增的avatarImageField 字段 ,Django 需要依赖 Pillow 进行图像处理,需要先执行如下命令,安装 Pillow 库。

pip install Pillow -i https://pypi.tuna.tsinghua.edu.cn/simple

安装完成后,进行数据迁移,会报如下错误。这是因为 User 是基础字段,需要先清空数据库,重新进行迁移。

image-20240805215916423

清空或重建数据库后,再次迁移不报错。

image-20240805215928059

其余地方用到 User 的地方,同样需要替换为 UserInfo,这是因为扩展 User 之后,原有的表消失,所有的字段信息都在新表上,因此需要我们将代码中应用到 User 的代码替换成 UserInfo

image-20240805222702736

再次启动 django,才能够正常运行并正常处理各项请求。

image-20240805222748950

这里发现了一个小瑕疵,当只有一页数据的时候,其实并不需要展示分页器,在 template/artcile/list.html 的文章列表内容做如下判断。

<!-- template/artcile/list.html -->

<!-- 只有当前分页数大于1,才需要展示分页器 -->
{% if page_list|length > 1 %}
<!-- 页码展示 -->
{% for item in page_list %}
<li class="page-item"><a class="page-link" href="?page={{ item }}">{{item}}</a></li>
{% endfor %}
{% endif %}

修改之后,效果如下。

image-20240805223240224

再次测试用户的注册登录,正常无报错。

用户登录

查看数据库,出现用户 euansu 的信息记录。

image-20240805223708531

2.6 userprofile 用户信息修改

首先则是编写用户信息修改的表单,在 userprofile/forms.py 新增 ProfileForm 也即用户修改的表单,写入电话、头像、简介、邮箱等信息。

# userprofile/forms.py
class ProfileForm(forms.ModelForm):
    class Meta:
        model = UserInfo
        fields = ('phone', 'avatar', 'bio', 'email')

修改 userprofile/views.py ,新增用户详情和用户修改两个路由函数,如下所示,这里引入了 login_requireddjango 提供的用户登录校验,按照装饰器的方式使用即可。

from django.contrib.auth.decorators import login_required

@login_required(login_url='/userprofile/login/')
def user_edit(request, id):
    user = UserInfo.objects.get(id=id)

    if request.method == 'POST':
        # 验证修改数据者,是否为用户本人
        if request.user != user:
            return HttpResponse("你没有权限修改此用户信息。")

        user_obj_form = UserEditForm(data=request.POST)
        if user_obj_form.is_valid():
            # 取得清洗后的合法数据
            user_obj_dic = user_obj_form.cleaned_data
            user.email = user_obj_dic['email']
            user.phone = user_obj_dic['phone']
            user.bio = user_obj_dic['bio']
            user.save()
            # 带参数的 redirect()
            # return redirect("userprofile:edit", id=id)
            return redirect("userprofile:detail", id=id)
        else:
            user_obj_form = UserEditForm()
            context = { 'userform': user_obj_form, 'user': user, 'msg':"用户表单填写有误,请重新输出~" }
            return render(request, 'userprofile/edit.html', context)

    elif request.method == 'GET':
        user_obj_form = UserEditForm()
        context = { 'userform': user_obj_form, 'user': user }
        return render(request, 'userprofile/edit.html', context)
    else:
        return HttpResponse("请使用GET或POST请求数据")
    
@login_required(login_url='/userprofile/login/')
def user_detail(request, id):
    if request.method == 'GET':
        user = UserInfo.objects.get(id=id)
        context = {"user":user}
        return render(request, 'userprofile/detail.html', context)
    else:
        return HttpResponse("请使用GET请求数据")

userprofile 目录下的路由文件增加用户信息展示和用户信息修改的路由函数。

# 用户修改
path('edit/<int:id>/', views.user_edit, name='edit'),
# 用户详情
path('detail/<int:id>/', views.user_detail, name='detail'),

template/userprofile/ 目录下新增 detail.htmledit.html 两个页面。

<!-- edit.html -->
{% extends "base.html" %} {% load static %}
{% block title %} 用户信息 {% endblock title %}
{% block content %}

<div class="container">
    <div class="row">
        <div class="col-4">
            <br>
            <div class="col-12">用户名: {{ user.username }}</div>
            <br>
            <div class="col-12">简介: {{ user.bio }}</div>
            <br>
            <div class="col-12">电话: {{ user.phone }}</div>
            <br>
        </div>
    </div>
</div>
{% endblock content %}

如下是 edit.html 页面。

<!-- edit.html -->
{% extends "base.html" %} {% load static %}
{% block title %} 用户信息 {% endblock title %}
{% block content %}

<!-- 失败告警 -->
{% if msg %}
<div class="container">
    <div class="row">
        <div class="col-6">
            <div class="alert alert-danger alert-dismissible fade show my-3" role="alert">
                {{msg}}
                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
            </div>  
        </div>
    </div>
</div>
{% endif %}

<div class="container">
    <div class="row justify-content-center">
        <div class="col-6">
            <br>
            <div class="col-12">用户名: {{ user.username }}</div>
            <br>
            <form method="post" action=".">
                {% csrf_token %}
                <!-- phone -->
                <div class="form-group col-12">
                    <label for="phone">电话</label>
                    <input type="text" class="form-control" id="phone" name="phone" value="{{ user.phone }}">
                </div>
                <!-- bio -->
                <div class="form-group col-12">
                    <label for="bio">简介</label>
                    <textarea type="text" class="form-control" id="bio" name="bio" rows="12">{{ user.bio }}</textarea>
                </div>
                <!-- 提交按钮 -->
                <button type="submit" class="btn btn-primary my-3">提交</button>
            </form>
        </div>
    </div>
</div>
{% endblock content %}

还需要在 header.html 也即标题栏增加用户信息展示和修改两个的入口,如下所示,在 dropdown 菜单的注销登录上面增加信息的查看和修改入口。

<!-- header.html -->
<ul class="dropdown-menu">
    <li><a class="dropdown-item" href='{% url "userprofile:detail" user.id %}'>信息查看</a></li>
    <li><a class="dropdown-item" href='{% url "userprofile:edit" user.id %}'>信息修改</a></li>
    <li><a class="dropdown-item" href='{% url "userprofile:logout" %}'>注销登录</a></li>
</ul>

实现效果如下。

image-20240805231526721

进行用户信息修改和查看的测试,如下。

信息修改

2.7 userprofile 用户头像上传

首先需要在 settings.py,也即项目的配置文件中添加静态资源的目录配置,如下所示。

# 存储媒体资源

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')

接下来则是编写头像的存储字段,在 2.5 userprofile 用户信息拓展 章节已经编写了该字段,其中 upload_to 则是存储图片的位置,也即 /media/avatar/%Y%m%d/ 目录下,这里及时上传同名文件也不用担心会出现冲突,Django 会在文件的最后默认添加唯一的标识符进行区分。

image-20240806214759622

# userprofile/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser

# 继承用户信息
class UserInfo(AbstractUser):
    ...
    # 头像
    avatar = models.ImageField(upload_to='avatar/%Y%m%d/', blank=True)

表单也已经写好了,如下所示。

# userprofile/forms.py
class ProfileForm(forms.ModelForm):
    class Meta:
        model = UserInfo
        fields = ('phone', 'avatar', 'bio', 'email')

视图文件 views.py 中需要添加对图像的处理,如下所示。

# 添加在user.save()之前即可
# 如果 request.FILES 存在文件,则保存
if 'avatar' in request.FILES:
    user.avatar = request.FILES['avatar']

接下来则需要在详情页对头像做展示,以及编辑页增加图像的上传和展示,如下所示。

<!-- detail.html -->
...
{% if user.avatar %}
<img src="{{ user.avatar.url }}" style="max-width: 20%; border-radius: 15%;" class="col-md-4">
<br>
{% else %}
<h5 class="col-6">暂无头像</h5>
{% endif %}
...

edit.html 还需要添加图像的上传操作。

<!-- edit.html -->
{% if user.avatar %}
<img src="{{ user.avatar.url }}" style="max-width: 20%; border-radius: 15%;" class="col-md-4">
<br>
{% else %}
<h5 class="col-6">暂无头像</h5>
{% endif %}
<br>
<form method="post" action="."  enctype="multipart/form-data">
    {% csrf_token %}
    <div class="form-group">
        <label for="avatar">上传头像</label>
        <input type="file" class="form-control-file" name="avatar" id="avatar">
    </div>
...

最终实现的效果如下所示。

上传头像

3.项目部署

3.1 部署操作

  • 准备服务器环境,安装对应版本的 Python

    # 解压Python安装包
    tar -zxvf Python-3.12.10.tgz
    # 进入解压后的目录
    cd Python-3.12.10
    # 配置安装的目录
    ./configure --prefix=/home/euansu/environment/Python-3.12 --enable-optimizations
    # 使用多线程进行编译
    make -j$(nproc)
    # 安装
    make install
  • 传送项目文件到部署主机

    scp -r DjangoBlog user@x.x.x.x:/home/user/apps
  • 创建虚拟环境并安装依赖

    # 创建虚拟环境
    ../environment/Python-3.12/bin/python3 -m venv DjangoEnv
    # 激活虚拟环境
    source DjangoEnv/bin/activate
    # 安装Python依赖
    pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
  • 配置 Django 项目,

    # settings.py
    # 这里需要替换为本机的ip
    ALLOWED_HOSTS = ['localhost'] 
    # 关闭DEBUG模式
    DEBUG = False
    # 配置静态文件目录(这里是为了下一步的收集静态文件做准备)
    STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
  • 数据库迁移 & 静态文件收集

    # 迁移数据库
    python manage.py migrate
    # 静态文件收集
    python manage.py collectstatic
  • nginx 代理静态项目路径

    server {
        listen 8000;
        server_name localhost;
    
        location /static/ {
            alias /www/DjangoBlog/staticfiles/;
        }
    
        location / {
            proxy_pass http://0.0.0.0:9999;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
  • 使用 Gunicorn 运行 Django 项目

    # 需要预先安装 gunicorn 依赖
    pip install gunicorn
    # 使用 gunicorn 运行django项目
    gunicorn DjangoBlog.wsgi:application --bind 127.0.0.1:9999 --workers 3

    部署的 Django 工程如下图所示:

    image-20250516015328436

    3.2 问题记录

    3.2.1 请求操作出现 500 报错

    访问Django 项目管理界面的时候,执行模型的添加操作,报了如下所示的错误信息。

    Snipaste_2025-05-16_01-49-21

    检查后台 Nginx 的错误日志信息,报错信息如下:

    2025/05/16 01:47:24 [crit] 768185#0: *111585 open() "/root/nginx/client_body_temp/0000000016" failed (13: Permission denied), client: 123.139.20.194, server: localhost, request: "POST /admin/article/category/add/ HTTP/1.1", host: "8.8.8.8:8000", referrer: "http://8.8.8.8:8000/admin/article/category/add/"

    提示进行请求的时候,出现了 Permission denied 的错误,这里是因为 Nginx 转发请求时使用的临时存储路径是 root 用户下的,普通用户没有办法访问,在 Nginxhttp 块添加临时存储路径的设置,如下所示:

    http {
        ...
        # 这里的路径需要提前进行创建
        # sudo mkdir -p /var/nginx/client_body_temp
    	#  sudo chown -R www-data:www-data /var/nginx/client_body_temp
        client_body_temp_path /var/nginx/client_body_temp;
        ...
    }

    再次重启 Nginx 后能够正常访问操作页面,如下所示:

    Snipaste_2025-05-16_01-52-07