Django搭建博客
Django 搭建博客
0.环境说明
Python 3.12.4
Django 5.0.70.1虚拟环境配置
配置虚拟环境
cd DjangoProject
python -m venv .venv激活虚拟环境
.\.venv\Scripts\activate
0.2 Django 安装
使用 pip 安装 django 时建议指定 pypi 源,否则安装速度可能会很慢。
pip install django -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后,使用 pip list 检查是否成功安装 Django。

0.3 Django 项目创建
这里需要激活虚拟环境后,再执行如下操作,创建 DjangoBlog 项目。
django-admin startproject DjangoBlog创建完成后,会生成如下的目录结构。
DjangoBlog
│ db.sqlite3
│ manage.py
│
└─DjangoBlog
│ settings.py
│ urls.py
│ wsgi.py
└─ __init__.py0.4 运行 Django 服务器
cd DjangoBlog
python manage.py runserver项目运行成功后,访问 127.0.0.1:8000 即可查看到如下界面。

1.article 文章管理应用
1.1 article 文章应用创建
激活虚拟环境后,在 DjangoBlog 目录下执行如下命令。
python manage.py startapp article
操作完成后,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__.py1.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
执行 migrate ,将迁移内容应用到数据库中。
python manage.py migrate
1.5 Django 网站后台管理
Django 内置了一个后台管理工具,只需要少量代码,就能够实现强大的功能。
1.5.1 创建管理员账号
管理员账号(Superuser)是可以进入网站后台,对数据进行维护的账号,具有很高的权限。
激活虚拟环境后,输入 python manage.py createsuperuser 创建管理员账号,过程中需要输入账号、密码、邮箱,如下所示。

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


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 数据表模型。

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

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

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/,就能够看到文章的列表。

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.htmlbase.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 © 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修改完成后,再次访问页面,就变成了如下内容。

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.htmldetail.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/ 页面,就能够看到文章详情的展示,并能够跳转文章列表。
点击阅读原文,跳转进入文章详情页。

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

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 语法。

上面的文章中,新增了 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
执行完成后,会在目录下生成一个名为 monokai.css 的样式文件。

在 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; /* 字体颜色设置为白色 */
}再次访问页面,能够看到代码高亮以及底边的颜色发生了变化。

1.10 artcile 文章发表页面
新增一个 artcile 的 form 表单,用来创建新文章。
# 引入表单类
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>实现的效果如下,导航栏右上方出现【新增文章】的字样,点击可以进行文章发表页面。

文章发表页面如下。

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

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

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

1.11 artcile 文章删除
修改 ArticlePost 模型,新增一个 is_deleted 属性,标记文章是否被删除。
# 标记文章是否被删除
is_deleted = models.BooleanField(default=False)新增如上属性后,执行文章的迁移操作,如下所示。
# 生成迁移文件
python manage.py makemigrations
# 应用迁移到数据库
python manage.py migrate
接下来编写文章删除的视图函数 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>
引入一个弹窗的插件库 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 的对话框。

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

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>接下来进行页面测试,点击修改文章,能够进入到文章修改页面。

修改内容后,点击提交。

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

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

修改 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">«</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">»</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>修改后的页面展示如下。

这样的效果还是有些不好,我们使用 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>修改后的目录展示如下。

2.userprofile 用户管理应用
2.1 userprofile 用户应用创建及注册
激活虚拟环境后,执行如下命令创建 userprofile 用户管理应用。
python manage.py startapp userprofile
在 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>实现的效果如下,输入的错误的密码。

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

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

接下来则是实现注销登录的操作,如上已经实现了对应的入口,这里只需要编写路由函数和视图函数即可,同时需要在前端点击注销的内容请求 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 %}页面显示的效果如下。

修改登录页 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 %}修改后的登录页面效果如下。

实际测试效果如下。

2.4 userprofile 重置用户密码
pip install django-password-reset -i https://pypi.tuna.tsinghua.edu.cn/simple2.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 的模型类。
此外,新增的avatar 是 ImageField 字段 ,Django 需要依赖 Pillow 进行图像处理,需要先执行如下命令,安装 Pillow 库。
pip install Pillow -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后,进行数据迁移,会报如下错误。这是因为 User 是基础字段,需要先清空数据库,重新进行迁移。

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

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

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

这里发现了一个小瑕疵,当只有一页数据的时候,其实并不需要展示分页器,在 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 %}修改之后,效果如下。

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

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

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_required,django 提供的用户登录校验,按照装饰器的方式使用即可。
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.html 和 edit.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>实现效果如下。

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

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 会在文件的最后默认添加唯一的标识符进行区分。

# 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 collectstaticnginx代理静态项目路径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工程如下图所示:
3.2 问题记录
3.2.1 请求操作出现 500 报错
访问
Django项目管理界面的时候,执行模型的添加操作,报了如下所示的错误信息。
检查后台
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用户下的,普通用户没有办法访问,在Nginx的http块添加临时存储路径的设置,如下所示: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后能够正常访问操作页面,如下所示: