BBS项目
BBS项目
项目的前期准备
1.django2.2 创建一个django目录
(需要配置环境变量和数据库)
'DIRS': [os.path.join(BASE_DIR, 'templates'), ]
2.数据库准备 我这里创建了一个库名为sql0107的数据库
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'sql0107',
'USER': 'root',
'PASSWORD': '222',
'HOST': '127.0.0.1',
'PORT': 3306,
'CHARSET': 'utf8',
}
}
第一步是创建表
表分析
先确定表的数量 再确定表的基础字段 最后确定表的外键字段
1.用户表
2.个人站点表
3.文章表
4.文章分类表
5.文章标签表
6.点赞点踩表
7.文章评论表
基础字段分析
'''下列表字段设计仅供参考 你可以有更多的想法'''
用户表
替换auth_user表并扩展额外的字段
电话号码、头像、注册时间
个人站点表
站点名称(jason\lili\kevin)
站点标题(努力奋斗去他妹的)
站点样式(css文件)
文章表
文章标题
文章简介
文章内容
发布时间
文章分类表
分类名称
文章标签表
标签名称
点赞点踩表:记录哪个用户给哪篇文章点了推荐(赞)还是反对(踩)
用户字段(用户主键)>>>:外键字段
文章字段(文章主键)>>>:外键字段
点赞点踩
文章评论表:记录哪个用户给哪篇文章评论了什么内容
用户字段(用户主键)>>>:外键字段
文章字段(文章主键)>>>:外键字段
评论内容
评论时间
外键字段(自关联)
"""
id user_id article_id content parent_id
1 1 1 哈哈哈 null
2 2 1 哈你妹 1
3 3 1 讲文明 2
"""
外键字段
用户表
用户与个人站点是一对一外键关系
个人站点表
文章表
文章表与个人站点表是一对多外键关系
文章表与文章分类表是一对多外键关系
文章表与文章标签表是多对多外键关系
'''
数据库字段优化设计:我们想统计文章的评论数 点赞数
通过文章数据跨表查询到文章评论表中对应的数据统计即可
但是文章需要频繁的展示 每次都跨表查询的话效率极低
我们在文章表中再创建三个普通字段
之后只需要确保每次操作评论表或者点赞点踩表时同步修改上述三个普通字段即可
'''
文章评论数
文章点赞数
文章点踩数
文章分类表
文章分类与个人站点是一对多外键关系
文章标签表
文章标签与个人站点是一对多外键关系
模型层代码:models.py
特别重要:
创建表之前因为用户表继承AbstractUser 要在设置里面加上
AUTH_USER_MODEL = 'app01.UserInfo'
模型层代码:
from django.db import models
# Create your models here.
from django.contrib.auth.models import AbstractUser
class UserInfo(AbstractUser):
"""用户表"""
phone = models.BigIntegerField(verbose_name='手机号', null=True, blank=True)
avatar = models.FileField(upload_to='media/avatar/', default='media/avatar/default.jpg', verbose_name='用户头像')
register_time = models.DateTimeField(verbose_name='注册时间', auto_now_add=True)
"""用户表与个人站点一对一外键"""
site = models.OneToOneField(to='Site', on_delete=models.CASCADE, null=True)
# 修改admin 后天管理的表名
# class Meta:
# verbose_name_plural = '用户表'
def __str__(self):
return f'用户对象:{self.username}'
class Site(models.Model):
"""个人站点表"""
site_name = models.CharField(verbose_name='站点名称', max_length=32)
site_title = models.CharField(verbose_name='站点标题', max_length=32)
site_theme = models.CharField(verbose_name='站点样式', max_length=32, null=True) # 模拟站点样式文件
# 修改admin 后台管理的表名
# class Meta:
# verbose_name_plural = '个人站点表'
def __str__(self):
return f'个人站点:{self.site_theme}'
class Article(models.Model):
"""文章表"""
title = models.CharField(verbose_name='文章标题', max_length=32)
desc = models.CharField(verbose_name='文章简介', max_length=255)
content = models.TextField(verbose_name='文章内容')
create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
# 三个优化字段
comment_num = models.IntegerField(verbose_name='评论数', default=0)
up_num = models.IntegerField(verbose_name='点赞数', default=0)
down_num = models.IntegerField(verbose_name='点踩数', default=0)
# 修改admin后台管理的表名
# class Meta:
# verbose_name_plural = '文章表'
"""文章与个人站点一对多外键"""
site = models.ForeignKey(to='Site', on_delete=models.CASCADE, null=True)
"""文章表与分类表外键"""
category = models.ForeignKey(to='Category', on_delete=models.CASCADE, null=True)
"""文章表与标签表是多对多 半自动创建"""
tags = models.ManyToManyField(to='Tag',
through='Article2Tag',
through_fields=('article', 'tag'), )
def __str__(self):
return f'文章对象:{self.title}'
class Category(models.Model):
"""文章分类表"""
name = models.CharField(verbose_name='分类名称', max_length=32)
"""个人站点与文章分类的外键"""
site = models.ForeignKey(to='Site', on_delete=models.CASCADE, null=True)
# 修改admin后台管理的表名
# class Meta:
# verbose_name_plural = '文章分类表'
def __str__(self):
return f'文章分类:{self.name}'
class Tag(models.Model):
"""文章标签表"""
name = models.CharField(verbose_name='标签名称', max_length=32)
"""个人站点和文章标签的外键"""
site = models.ForeignKey(to='Site', on_delete=models.CASCADE, null=True)
# 修改admin后台管理的表名
# class Meta:
# verbose_name_plural = '文章标签表'
def __str__(self):
return f'文章标签:{self.name}'
"""文章与标签表是多对多 半自动创建"""
class Article2Tag(models.Model):
article = models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True)
tag = models.ForeignKey(to='Tag', on_delete=models.CASCADE, null=True)
# 修改admin后台管理的表名
# class Meta:
# verbose_name_plural = '文章表与标签表关系'
class UpAndDown(models.Model):
"""点赞点踩表"""
user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE, null=True)
article = models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True)
is_up = models.BooleanField(verbose_name='点赞点踩')
# # 修改admin后台管理的表名
# class Meta:
# verbose_name_plural = '文章点赞点踩表'
class Comment(models.Model):
"""文章评论表"""
user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE, null=True)
article = models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True)
content = models.TextField(verbose_name='评论内容')
comment_time = models.DateTimeField(auto_now_add=True, verbose_name='评论时间')
parent = models.ForeignKey(to='self', on_delete=models.CASCADE, null=True)
# # 修改admin后台管理的表名
# class Meta:
# verbose_name_plural = '文章评论表'
然后执行数据库迁移命令 添加数据库 (这里使用mysql)
注册功能
1.先开启路由
2.返回一个用户的注册页面
3.引入静态文件 创建static 引入bootstrap文件
4.构建前端注册页面
1.渲染前端标签 (采用一个空的form标签)
2.获取用户数据
3.校验用户数据
4.展示错误提示
ps: 用forms组件、modelform组件
单独开设接口进行编写 解耦合
5.用户头像的实时展示 固定代码 注意标签名字
6.给注册按钮绑定点击事件
1.循环form标签内数据 添加数据 加上头像文件
2.发送ajax请求携带文件数据必要配置
contentType:false,
processData: false,
注册完成后跳转
循环发送form内数据
渲染错误信息(查找标签)
3.给所有标签渲染错误样式 聚焦移除错误样式
7.后端代码的编写
用户上传头像的判断
创建信息时注意用create_user 替换了auth表(**clean_data)
forms校验文件代码
单独创建一个myforms.py文件
from django import forms
from app01 import models
from django.forms import widgets
class RegisterForm(forms.Form):
"""用户注册form类"""
username = forms.CharField(min_length=3, max_length=8, label='用户注册',
error_messages={
'min_length': '用户名最短三位',
'max_length': '用户名最长八位',
'required': '用户名不能为空',
}, widget=forms.widgets.TextInput(attrs={'class': 'form-control'})
)
password = forms.CharField(min_length=3, max_length=8, label='密码',
error_messages={
'min_length': '密码最短三位',
'max_length': '密码最长八位',
'required': '密码不能为空',
}, widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})
)
confirm_password = forms.CharField(min_length=3, max_length=8, label='确认密码',
error_messages={
'min_length': '密码最短三位',
'max_length': '密码最长八位',
'required': '密码不能为空',
}, widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})
)
email = forms.EmailField(label='邮箱',
error_messages={
'required': '邮箱不能为空',
'invalid': '邮箱格式不正确',
}, widget=forms.widgets.EmailInput(attrs={'class': 'form-control'}))
# 用户头像单独校验 不使用校验类 其他字段自己看加校验
# 钩子函数
# 局部钩子校验用户名是否已存在
def clean_username(self):
username = self.cleaned_data.get('username')
res = models.UserInfo.objects.filter(username=username)
if res:
self.add_error('username', '用户名已存在')
return username
# 全局钩子校验两次密码是否一致
def clean(self):
password = self.cleaned_data.get('password')
confirm_password = self.cleaned_data.get('confirm_password')
if not password == confirm_password:
self.add_error('confirm_password', '两次密码不一致')
return self.cleaned_data
以下前端校验的截图 有几个点需要注意一下:
注册功能前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
{% load static %}
<link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}">
<script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script>
</head>
<body>
<div class="container">
<div class="col-md-8 col-md-offset-2">
<h2 class="text-center">用户注册</h2>
<form id="form"> <!--不使用form表单提交数据 但是用一下form标签 它有一个序列化功能-->
{% csrf_token %}
{% for form in form_obj %}
<div class="form-group"> <!--目的是让多个获取用户数据的标签上下间距更大一些-->
<label for="{{ form.auto_id }}">{{ form.label }}</label> <!--form.auto_id 自动获取渲染的标签id值-->
{{ form }}
<span style="color: red" class="pull-right"></span>
</div>
{% endfor %}
<!--用户头像自己编写相关标签获取-->
<div class="form-group">
<label for="myfile">头像
<img src="/static/img/default.jpg" alt="" id="myimg" width="120">
</label>
<input type="file" id="myfile" style="display: none" >
</div>
<input type="button" id="subBtn" class="btn btn-primary btn-block" value="用户注册">
</form>
</div>
</div>
<script>
//1.用户头像的实时展示
$('#myfile').change(function (){
//1.产生一个文件阅读器对象
let myFilReaderObj = new FileReader();
// 2.获取用户上传的头像文件
let fileObj = this.files[0];
//3.将文件对象交给阅读器对象读取
myFilReaderObj.readAsDataURL(fileObj); //异步
// 等待文件阅读器对象加载完毕之后再修改src
myFilReaderObj.onload = function (){
//4.修改img标签的src 属性展示图片
$('#myimg').attr('src',myFilReaderObj.result)
}
})
//2.给注册按钮绑定点击事件 发送ajax 携带了文件数据
$('#subBtn').click(function (){
//1.先产生一个内置对象
let myFormDataObj = new FormData();
//2.添加普通数据(单个单个的编写效率极低)
{#console.log($('#form').serializeArray()) // 可以一次性获取form标签内所有普通字段数据 [{数组},{数组},{数组}]#}
$.each($('#form').serializeArray(),function (index,dataObj){ // 对结果for循环 然后交给后面的函数处理
myFormDataObj.append(dataObj.name,dataObj.value) // {'name':'','value':'',}
})
//3.添加文件数据
myFormDataObj.append('avatar',$('#myfile')[0].files[0])
//4.发送ajax请求
$.ajax({
url:'',
type: 'post',
data:myFormDataObj,
contentType:false,
processData: false,
success: function (args){
if(args.code === 10000){
window.location.href = args.url
}else{
{#console.log(args.msg)#}
let dataObj = args.msg;
// 如何针对性的渲染错误提示 {'username'} id_username
$.each(dataObj,function (k,msgArray){
// 拼接标签的id值
let eleId = '#id_' + k
// 根据id查找标签 修改下面span标签的内容 并给父标签添加错误样式
$(eleId).next().text(msgArray[0]).parent().addClass('has-error')
})
}
}
})
})
//3.给所有的input标签绑定获取焦点事件 移除错误样式
$('input').focus(function (){
$(this).next().text('').parent().removeClass('has-error')
})
</script>
</body>
</html>
注册功能后端代码
from django.shortcuts import render, HttpResponse, redirect
from app01 import models
from app01 import myforms
from django.http import JsonResponse
def register_func(request):
# 前后端ajax交互 通常采用字段作为交互对象
back_dict = {'code': 10000, 'msg': ''}
# 1.先产生一个空的form_obj
form_obj = myforms.RegisterForm()
if request.method == 'POST':
form_obj = myforms.RegisterForm(request.POST) # username password confirm_password email csrfmiddlewaretoken
if form_obj.is_valid():
clean_data = form_obj.cleaned_data # 存储符合校验的数据 {username password confirm_password email}
# 将confirm_password键值对移除
clean_data.pop('confirm_password') # {username password email}
# 获取用户上传的头像文件
avatar_obj = request.FILES.get('avatar') # 用户有可能没有上传
if avatar_obj:
clean_data['avatar'] = avatar_obj # {username password email avatar}
# 创建用户数据
models.UserInfo.objects.create_user(**clean_data) # 上述处理字典的目的就是为了创建数据省事
# return HttpResponse('注册成功!!!!')
back_dict['msg'] = '注册功能'
back_dict['url'] = '/login/'
else:
back_dict['code'] = 10001
back_dict['msg'] = form_obj.errors
return JsonResponse(back_dict)
return render(request, 'registerPage.html', locals())
登录功能
1.开设路由
2.后端先返回一个loginPage.html页面
3.登录页面搞一个图片验证码的效果(后端代码编写)
需要用到:1.pillow模块
2.io模块
3.random模块
下载模块经常会报错 因为源有问题
这里直接终端执行 注意模块名手写
pip3.8 install 模块名 -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
点击图片实时刷新验证码
保存验证码便于后端校验
4.渲染登录按钮
发送ajax请求
正确就跳转到主页
或者显示错误提示
1.alert(错误内容)
2.使用sweetalert 插件美化展示 需要引入sweeralert cdn
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
5.后端代码编写
auth模块帮助校验
1.先搞个验证字典
back_dict = {'code': 10000, 'msg': ''}
2.获取前端数据 比对
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
code = request.POST.get('code')
if code.upper() == request.session.get('code').upper():
user_obj = auth.authenticate(request, username=username, password=password)
3.保存用户登录状态
4.错误信息添加
路由展示
# 登录功能
path('login/', views.login_func, name='login_view'),
# 图片验证码相关功能
path('get_code/', views.get_code_func),
图片验证码代码:
from PIL import Image, ImageFont, ImageDraw
"""
Image: 产生图片
ImageFont: 字体样式
ImageDraw: 画笔对象
"""
from io import BytesIO, StringIO
"""
BytesIO: 在内存中临时存储 读取的时候以bytes格式为准
StringIO: 在内存中临时存储 读取的时候以字符串格式为准
"""
import random
def get_random():
return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)
def get_code_func(request):
# 1.推导步骤1:直接读取图片文件返回
# with open(r'D:\djangoProject\BBS\app01\avatar\555.jpg', 'rb') as f:
# data = f.read()
# return HttpResponse(data)
# # 2.推导步骤2:随机产生图片动态返回 pillow模块
# img_obj = Image.new('RGB',(350,35),'yellow')
# with open(r'xxx.png','wb')as f:
# img_obj.save(f,'png')
# with open(r'xxx.png','rb')as f:
# data = f.read()
# return HttpResponse(data)
# 3.推导步骤3:针对图片的保存与读取 做优化 内存管理器
# img_obj = Image.new('RGB', (350, 35), 'yellow')
# io_obj = BytesIO()
# img_obj.save(io_obj, 'png')
# return HttpResponse(io_obj.getvalue())
# 4.推导步骤4:图片颜色可以随机变换
# img_obj = Image.new('RGB', (350, 35), get_random())
# io_obj = BytesIO()
# img_obj.save(io_obj, 'png')
# return HttpResponse(io_obj.getvalue())
# 5.推导步骤5:编写验证码
img_obj = Image.new('RGB', (350, 35), get_random()) # 先产生图片对象
# 将图片对象交给画笔对象
draw_obj = ImageDraw.Draw(img_obj)
# 确定字体样式(ttf文件)
font_obj = ImageFont.truetype('static/font/111.ttf', 35)
# 产生随机验证码
code = ''
for i in range(5):
random_upper = chr(random.randint(65, 90))
random_lower = chr(random.randint(97, 122))
random_int = str(random.randint(1, 9))
# 三选一
temp_choice = random.choice([random_upper, random_lower, random_int])
# 写到图片上
draw_obj.text((i * 45 + 45, 0), temp_choice, font=font_obj)
code += temp_choice
# 后端保存验证码 便于后续的比对
request.session['code'] = code
io_obj = BytesIO()
img_obj.save(io_obj, 'png')
return HttpResponse(io_obj.getvalue())
前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
{% load static %}
<link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}">
<script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script>
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
</head>
<body>
<div class="container">
<div class="col-md-8 col-md-offset-2">
<h2 class="text-center">用户登录</h2>
{% csrf_token %}
<div class="form-group">
<label for="name">用户名</label>
<input type="text" id="name" class="form-control" name="username">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" class="form-control" name="password">
</div>
<div class="form-group">
<label for="code">验证码</label>
<div class="row">
<div class="col-md-6">
<input type="text" id="code" class="form-control" name="code">
</div>
<div class="col-md-6">
<img src="/get_code/" alt="" width="350" id="d1" height="35">
</div>
</div>
</div>
<input type="button" class="btn btn-success btn-block" value="用户登录" id="loginBtn">
</div>
</div>
<script>
// 1.图片验证码动态刷新
$('#d1').click(function () {
let oldSrc = $(this).attr('src');
$(this).attr('src', oldSrc + '?')
})
// 2.登录按钮发送ajax请求
$('#loginBtn').click(function () {
// 可以再次使用form标签序列化功能 也可以自己挨个获取
$.ajax({
url: '',
type: 'post',
data: {
'username': $('#name').val(),
'password': $('#password').val(),
'code': $('#code').val(),
'csrfmiddlewaretoken': '{{ csrf_token }}'
},
success: function (args) {
if (args.code === 10000) {
window.location.href = args.url
} else {
// 课下可以使用sweetalert 插件美化展示 需要引入sweetalert cdn
alert(args.msg)
{#swal(args.msg,'验证码输错了!!!')#}
}
}
})
})
</script>
</body>
</html>
后端代码(获取图片验证码的代码在上面)
import json
from django.shortcuts import render, HttpResponse, redirect
from app01 import models
from app01 import myforms
from django.http import JsonResponse
from django.contrib import auth
def login_func(request):
back_dict = {'code': 10000, 'msg': ''}
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
code = request.POST.get('code')
if code.upper() == request.session.get('code').upper():
user_obj = auth.authenticate(request, username=username, password=password)
if user_obj:
# 保存用户登录状态
auth.login(request, user_obj) # 执行之后就可以使用request.user获取登录用户对象
back_dict['msg'] = '登录成功!!!!'
back_dict['url'] = '/home/'
else:
back_dict['code'] = 10001
back_dict['msg'] = '用户名或密码不正确'
else:
back_dict['code'] = 10002
back_dict['msg'] = '验证码错误'
return JsonResponse(back_dict)
return render(request, 'loginPage.html')
网页主页
1.开设路由
# 网站首页
path('home/', views.home_func, name='home_view'),
2.返回html页面
3.首页导航条
1.引入静态文件
2.复制导航条页面
3.后端导入auth模块配合
from django.contrib import auth
文章过多的话可以添加 分页器
创建一个mypage.py的文件 CV代码
4.修改密码
1.开设路由
# 修改密码功能
path('set_pwd/', views.set_pwd_func),
2.使用装饰器进行校验是否登录
from django.contrib.auth.decorators import login_required
@login_required
这个模块可以在全局设置里面配置
LOGIN_URL = '/login/'
3.考虑用模态框直接进行修改 需要到 v3里面复制粘贴
4.前端发送ajax请求
5.后端代码获取前端修改的数据 进行处理 错误信息添加到 back_dict字典内
6.前端错误信息渲染
5.修改头像
1.开设修改头像路由 配置暴露资源接口
# 用户头像修改
path('set_avatar/', views.set_avatar_func),
from django.views.static import serve
from django.conf import settings
# 自定义暴露资源接口
re_path('media/(?P<path>.*)', serve, {'document_root': settings.MEDIA_ROOT}),
还需要在全局配置里配置 MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
2.使用装饰器进行校验是否登录
from django.contrib.auth.decorators import login_required
@login_required
3.考虑用模态框直接进行修改 需要到 v3里面复制粘贴
头像的实时展示 和注册时一样
4.前端发送ajax请求
5.后端代码获取前端修改的数据 进行处理 错误信息添加到 back_dict字典内
6.前端错误信息渲染
6.注销登录
1.开设路由
# 注销登录功能
path('logout/', views.logout),
2.后端代码用auth模块进行处理 跳转到主页
3.注销登录的按钮路由添加
4.主页内容的添加(可以考虑把内容区域继承 block )
1.先分区(两边是广告 中间是文章 需要先在数据库里面绑定 使用命令创建超级管理员 createsuperuser)
2.准备数据 需要在admin.py中添加表的绑定关系 在表中绑数据 (注意个人站点绑定别绑错了)
from app01 import models
dmin.site.register(models.UserInfo)
admin.site.register(models.Site)
admin.site.register(models.Article)
admin.site.register(models.Category)
admin.site.register(models.Tag)
admin.site.register(models.Article2Tag)
admin.site.register(models.UpAndDown)
admin.site.register(models.Comment)
3.添加用户名 时间 点赞 点踩 评论等
首页功能前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
{% load static %}
<link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}">
<script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script>
{% block css %}
{% endblock %}
</head>
<body>
<!--导航条开始-->
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">
{% block title %}
BBS
{% endblock %}
</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="#">博客 <span class="sr-only">(current)</span></a></li>
<li><a href="#">文章</a></li>
</ul>
<form class="navbar-form navbar-left">
<div class="form-group">
<input type="text" class="form-control" placeholder="搜索">
</div>
<button type="submit" class="btn btn-default">搜索</button>
</form>
<ul class="nav navbar-nav navbar-right">
{% if request.user.is_authenticated %}
<li><a href="#">{{ request.user.username }}</a></li>
<li style="width: 38px;height: 38px;border-radius: 50%; overflow: hidden;display: block"><img src="/media/{{ request.user.avatar }}/" alt="" style="max-width: 100%"></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
aria-expanded="false">更多操作 <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#" data-toggle="modal" data-target="#myModal">修改密码</a></li>
<li><a href="#" data-toggle="modal" data-target="#myavatar">修改头像</a></li>
<li><a href="/backend/">后台管理</a></li>
<li role="separator" class="divider"></li>
<li><a href="/logout/">注销登录</a></li>
</ul>
</li>
{% else %}
<li><a href="{% url 'register_view' %}">注册</a></li>
<li><a href="{% url 'login_view' %}">登录</a></li>
{% endif %}
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
<!--导航条结束-->
<!--模态框开始-->
<!--修改密码模态框-->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span>
</button>
<h4 class="modal-title text-center" id="myModalLabel">修改密码</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label for="">用户名</label>
<input type="text" value="{{ request.user.username }}" disabled class="form-control">
</div>
<div class="form-group">
<label for="">原密码</label>
<input type="text" id="old_pwd" class="form-control">
</div>
<div class="form-group">
<label for="">新密码</label>
<input type="text" id="new_pwd" class="form-control">
</div>
<div class="form-group">
<label for="">确认密码</label>
<input type="text" id="confirm_pwd" class="form-control">
</div>
</div>
<div class="modal-footer">
<span id="error" style="color: red" ></span>
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-warning" id="setBtn">修改</button>
</div>
</div>
</div>
</div>
<!--改头像模态框-->
<div class="modal fade" id="myavatar" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span>
</button>
<h4 class="modal-title text-center" id="myModalLabel">修改头像</h4>
</div>
<div class="modal-body">
<form action="/set_avatar/" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-group">
<label for="">用户名</label>
<input type="text" value="{{ request.user.username }}" disabled class="form-control">
</div>
<div class="form-group">
<label for="">原头像</label>
<img src="/media/{{ request.user.avatar }}" alt="" width="120">
</div>
<div class="form-group">
<label for="myfile">新头像
<img src="/static/img/default.jpg" alt="" id="myimg" width="120">
</label>
<input type="file" id="myfile" style="display: none" name="new_avatar" >
</div>
<input type="submit" class="btn btn-warning btn-block">
</form>
</div>
</div>
</div>
</div>
<!--模态框结束-->
<!--内容区开始-->
<div class="container-fluid">
<div class="row">
{% block content %}
<div class="col-md-2">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">中秋节苦熬了</h3>
</div>
<div class="panel-body">
抓紧练习:wuyong
</div>
</div>
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">百万大奖</h3>
</div>
<div class="panel-body">
共享你幸运儿:22222
</div>
</div>
<div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">广告找找</h3>
</div>
<div class="panel-body">
旺铺难求
</div>
</div>
</div>
<div class="col-md-8">
{% for article_obj in page_queryset %}
<div class="media">
<h4 class="media-heading"><a href="/{{ article_obj.site.userinfo.username }}/article/{{ article_obj.pk }}">{{ article_obj.title }}</a></h4>
<div class="media-left">
<a href="#">
<img class="media-object" src="/media/{{ article_obj.site.userinfo.avatar }}" alt="..." width="80">
</a>
</div>
<div class="media-body">
{{ article_obj.desc }}
</div>
<br>
<div>
<span><a href="/{{ article_obj.site.userinfo.username }}/">{{ article_obj.site.userinfo.username }} </a></span>
<span>{{ article_obj.create_time|date:'Y-m-d H:i:s' }} </span>
<span class="glyphicon glyphicon-thumbs-up">{{ article_obj.up_num }} </span>
<span class="glyphicon glyphicon-thumbs-down">{{ article_obj.down_num }} </span>
<span class="glyphicon glyphicon-comment">{{ article_obj.comment_num }} </span>
</div>
</div>
<hr>
{% endfor %}
<div class="text-center">{{ page_obj.page_html|safe }}</div>
</div>
<div class="col-md-2">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">中秋节苦熬了</h3>
</div>
<div class="panel-body">
抓紧练习:wuyong
</div>
</div>
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">百万大奖</h3>
</div>
<div class="panel-body">
共享你幸运儿:22222
</div>
</div>
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title">广告找找</h3>
</div>
<div class="panel-body">
旺铺难求
</div>
</div>
</div>
{% endblock %}
</div>
</div>
<!--内容区结束-->
<script>
$('#setBtn').click(function () {
$.ajax({
url: '/set_pwd/',
type: 'post',
data: {
'old_pwd': $('#old_pwd').val(),
'new_pwd': $('#new_pwd').val(),
'confirm_pwd': $('#confirm_pwd').val(),
'csrfmiddlewaretoken': '{{ csrf_token }}',
},
success: function (args) {
if(args.code === 10000){
window.location.href = args.url
}else{
$('#error').text(args.msg)
}
}
})
})
//1.用户头像的实时展示
$('#myfile').change(function (){
//1.产生一个文件阅读器对象
let myFileReaderObj = new FileReader();
// 2.获取用户上传的头像文件
let fileObj = this.files[0];
//3.将文件对象交给阅读器对象读取
myFileReaderObj.readAsDataURL(fileObj); //异步
// 等待文件阅读器对象加载完毕之后再修改src
myFileReaderObj.onload = function (){
//4.修改img标签的src 属性展示图片
$('#myimg').attr('src',myFileReaderObj.result)
}
})
</script>
{% block js %}
{% endblock %}
</body>
</html>
首页功能后端代码
分页器代码
class Pagination(object):
def __init__(self, current_page, all_count, per_page_num=10, pager_count=11):
"""
封装分页相关数据
:param current_page: 当前页
:param all_count: 数据库中的数据总条数
:param per_page_num: 每页显示的数据条数
:param pager_count: 最多显示的页码个数
"""
try:
current_page = int(current_page)
except Exception as e:
current_page = 1
if current_page < 1:
current_page = 1
self.current_page = current_page
self.all_count = all_count
self.per_page_num = per_page_num
# 总页码
all_pager, tmp = divmod(all_count, per_page_num)
if tmp:
all_pager += 1
self.all_pager = all_pager
self.pager_count = pager_count
self.pager_count_half = int((pager_count - 1) / 2)
@property
def start(self):
return (self.current_page - 1) * self.per_page_num
@property
def end(self):
return self.current_page * self.per_page_num
def page_html(self):
# 如果总页码 < 11个:
if self.all_pager <= self.pager_count:
pager_start = 1
pager_end = self.all_pager + 1
# 总页码 > 11
else:
# 当前页如果<=页面上最多显示11/2个页码
if self.current_page <= self.pager_count_half:
pager_start = 1
pager_end = self.pager_count + 1
# 当前页大于5
else:
# 页码翻到最后
if (self.current_page + self.pager_count_half) > self.all_pager:
pager_end = self.all_pager + 1
pager_start = self.all_pager - self.pager_count + 1
else:
pager_start = self.current_page - self.pager_count_half
pager_end = self.current_page + self.pager_count_half + 1
page_html_list = []
# 添加前面的nav和ul标签
page_html_list.append('''
<nav aria-label='Page navigation>'
<ul class='pagination'>
''')
first_page = '<li><a href="?page=%s">首页</a></li>' % (1)
page_html_list.append(first_page)
if self.current_page <= 1:
prev_page = '<li class="disabled"><a href="#">上一页</a></li>'
else:
prev_page = '<li><a href="?page=%s">上一页</a></li>' % (self.current_page - 1,)
page_html_list.append(prev_page)
for i in range(pager_start, pager_end):
if i == self.current_page:
temp = '<li class="active"><a href="?page=%s">%s</a></li>' % (i, i,)
else:
temp = '<li><a href="?page=%s">%s</a></li>' % (i, i,)
page_html_list.append(temp)
if self.current_page >= self.all_pager:
next_page = '<li class="disabled"><a href="#">下一页</a></li>'
else:
next_page = '<li><a href="?page=%s">下一页</a></li>' % (self.current_page + 1,)
page_html_list.append(next_page)
last_page = '<li><a href="?page=%s">尾页</a></li>' % (self.all_pager,)
page_html_list.append(last_page)
# 尾部添加标签
page_html_list.append('''
</nav>
</ul>
''')
return ''.join(page_html_list)
首页视图代码
from django.shortcuts import render, HttpResponse, redirect
from django.http import JsonResponse
from django.contrib import auth
from django.contrib.auth.decorators import login_required
from app01 import mypage
def home_func(request):
# 查询所有的用户编写的文章
article_queryset = models.Article.objects.all()
'''文章过多的情况下应该考虑添加分页器'''
page_obj = mypage.Pagination(current_page=request.GET.get('page'), all_count=article_queryset.count())
page_queryset = article_queryset[page_obj.start:page_obj.end]
return render(request, 'homePage.html', locals())
@login_required
def set_pwd_func(request):
back_dict = {'code': 10000, 'msg': ''}
if request.method == 'POST':
old_pwd = request.POST.get('old_pwd')
new_pwd = request.POST.get('new_pwd')
confirm_pwd = request.POST.get('confirm_pwd')
# 先校验原密码是否正确
if request.user.check_password(old_pwd):
# 再校验两次密码是否一致 并且不能为空
if new_pwd == confirm_pwd and new_pwd:
request.user.set_password(new_pwd)
request.user.save()
back_dict['msg'] = '密码修改成功!!!'
back_dict['url'] = '/login/'
else:
back_dict['code'] = 10001
back_dict['msg'] = '两次密码不一致或者为空'
else:
back_dict['code'] = 10002
back_dict['msg'] = '原密码错误'
return JsonResponse(back_dict)
@login_required
def set_avatar_func(request):
new_avatar = request.FILES.get('new_avatar')
# update 不会自动加avatar前缀
# models.UserInfo.objects.filter(pk=request.user.pk).update(avatar=new_avatar)
user_obj = models. UserInfo.objects.filter(pk=request.user.pk).first()
user_obj.avatar = new_avatar
user_obj.save() # 保存数据
return redirect('/backend/')
@login_required
def logout(request):
auth.logout(request)
return redirect('home_view')
个人站点功能
1.开设个人站点路由
# 个人站点接口
path('<str:username>/', views.site_func),
# 侧边栏筛选接口
# path('<str:username>/category/<int:category_id>/', views.site_func),
# path('<str:username>/tag/<int:tag_id>/', views.site_func),
# path('<str:username>/archive/<str:yearAndmonth>/', views.site_func),
# # 上述三个路由可以合并成一个路由
re_path('^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<params>.*?)/', views.site_func),
2.因为没有写文章添加功能 先用admin账号进行数据绑定 注意个人站点和个人用户绑在一起 然后文章标签、文章分类需要做区分
3.进行前端页面渲染 需要继承首页的导航条
4.个人站点有可能选择的错误的 后端做一个错误404展示
5.因为个人站点和个人文章详情需要相同相同的侧边栏渲染 文章标签、分类、和文章的日期 选择做一个模板页 引入一个inclusion_tag
from django import template
register = template.Library()
@register.inclusion_tag('leftmenu.html', name='mymenu')
注意:文章日期需要做一个日期筛选 django默认时区需要修改两个配置
# TIME_ZONE = 'UTC' 默认
TIME_ZONE = 'Asia/Shanghai' 修改
# USE_TZ = True 默认
USE_TZ = False 修改
6.后端代码的书写
封装inclusion_tag模板代码 和前端代码
前端代码 leftmenu.html
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">文章分类</h3>
</div>
<div class="panel-body">
{% for category_obj in category_queryset %}
<p><a href="/{{ site_obj.site_name }}/category/{{ category_obj.pk }}/">{{ category_obj.name }}({{ category_obj.article_num }})</a></p>
{% endfor %}
</div>
</div>
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">文章标签</h3>
</div>
<div class="panel-body">
{% for tag_obj in tag_queryset %}
<p><a href="/{{ site_obj.site_name }}/tag/{{ tag_obj.pk }}/">{{ tag_obj.name }}({{ tag_obj.article_num }})</a></p>
{% endfor %}
</div>
</div>
<div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">日期归档</h3>
</div>
<div class="panel-body">
{% for date_obj in date_queryset %}
<p><a href="/{{ site_obj.site_name }}/archive/{{ date_obj.month|date:'Y-m' }}/">{{ date_obj.month|date:'Y年m月' }}({{ date_obj.article_num }})</a></p>
{% endfor %}
</div>
</div>
templatetags 文件下 mytag.py代码
from django import template
from app01 import models
from django.db.models import Count
from django.db.models.functions import TruncMonth
register = template.Library()
@register.inclusion_tag('leftmenu.html', name='mymenu')
def index(username):
site_obj = models.Site.objects.filter(site_name=username).first()
# 查询个人站点下所以的分类名称以及每个分类下的文章数
category_queryset = models.Category.objects.filter(site=site_obj).annotate(article_num=Count('article__pk')).values(
'name', 'article_num', 'pk')
# 查询个人站点下所有的标签名称以及每个标签下的文章数
tag_queryset = models.Tag.objects.filter(site=site_obj).annotate(article_num=Count('article__pk')).values(
'name', 'article_num', 'pk')
# 年月分组并统计文章个数
date_queryset = models.Article.objects.filter(site=site_obj).annotate(month=TruncMonth('create_time')).values(
'month').annotate(
article_num=Count('pk')).values('month', 'article_num')
return locals()
个人站点前端代码
{% extends 'homePage.html' %}
{% block css %}
<link rel="stylesheet" href="media/css/{{ site_obj.site_theme }}/">
{% endblock %}
{% block title %}
{{ site_obj.site_title }}
{% endblock %}
{% block content %}
<div class="col-md-2">
{% load mytag %}
{% mymenu username %}
</div>
<div class="col-md-10">
{% for article_obj in article_queryset %}
<div class="media">
<h4 class="media-heading"><a href="/{{ article_obj.site.userinfo.username }}/article/{{ article_obj.pk }}">{{ article_obj.title }}</a></h4>
<div class="media-left">
<a href="#">
<img class="media-object" src="/media/{{ article_obj.site.userinfo.avatar }}" alt="..." width="80">
</a>
</div>
<div class="media-body" style="padding: 10px">
{{ article_obj.desc }}
</div>
<br>
<div class="pull-right">
<span>posted @</span>
<span>{{ article_obj.create_time|date:'Y-m-d H:i:s' }} </span>
<span>{{ article_obj.site.userinfo.username }} </span>
<span class="glyphicon glyphicon-thumbs-up">{{ article_obj.up_num }} </span>
<span class="glyphicon glyphicon-thumbs-down">{{ article_obj.down_num }} </span>
<span class="glyphicon glyphicon-comment">{{ article_obj.comment_num }} </span>
</div>
</div>
<hr>
{% endfor %}
</div>
{% endblock %}
个人站点后端代码
def site_func(request, username, **kwargs):
"""
:param kwargs: 接收多余的关键字参数 代码通过该参数是否有值从而得出是个人站点还是侧边栏筛选
"""
# print(kwargs)
# 查询个人站点是否存在
site_obj = models.Site.objects.filter(site_name=username).first()
if not site_obj:
return render(request, 'errorPage.html')
# 查询个人站点下所有的文章
article_queryset = models.Article.objects.filter(site=site_obj)
if kwargs:
condition = kwargs.get('condition')
params = kwargs.get('params')
if condition == 'category':
article_queryset = article_queryset.filter(category_id=params)
elif condition == 'tag':
article_queryset = article_queryset.filter(tags__pk=params)
else: # 年-月
year, month = params.split('-')
article_queryset = article_queryset.filter(create_time__year=year, create_time__month=month)
'''如果文章较多 加分页器'''
return render(request, 'sitePage.html', locals())
文章详情页功能
1.开设一个路由
# 文章详情页
path('<str:username>/article/<str:article_id>/', views.article_detail_func),
2.前端代码展示
1.需要结合inclusion_tag进行页面渲染
2.文章内容渲染
3.文章点赞点踩样式渲染
1.开设路由
# 文章点赞点踩
path('up_or_down/', views.up_or_down_func),
2.在css代码处添加 点赞点踩样式
3.在内容区域添加点赞点踩样式
4.绑定点赞点踩按钮操作 发送ajax请求
5.后端代码
判断用户是否登录
判断当前文章是否是当前登录用户的
校验当前文章是否被当前用户点赞点踩过
创建点赞点踩记录(不要忘记文章表中的优化字段 同步自增)
判断错误情况 前端ajax进行判断渲染错误信息
4.文章评论渲染
1.开设路由
# 文章评论
path('comment/', views.comment_func),
2.检验用户是否登陆 前端样式渲染
3.文章评论楼的渲染
4.文章子评论的渲染及评论的实时展示
5.后端代码的编写
**个人站点的前端代码 包括点赞点踩样式 评论样式 **
{% extends 'homePage.html' %}
{% block css %}
<link rel="stylesheet" href="media/css/{{ site_obj.site_theme }}/">
<style>
#div_digg {
float: right;
margin-bottom: 10px;
margin-right: 30px;
font-size: 12px;
width: 125px;
text-align: center;
margin-top: 10px;
}
.diggit {
float: left;
width: 46px;
height: 52px;
background: url('/static/img/upup.gif') no-repeat;
text-align: center;
cursor: pointer;
margin-top: 2px;
padding-top: 5px;
}
.clear {
clear: both;
}
.buryit {
float: right;
margin-left: 20px;
width: 46px;
height: 52px;
background: url('/static/img/downdown.gif') no-repeat;
text-align: center;
cursor: pointer;
margin-top: 2px;
padding-top: 5px;
}
.diggword {
margin-top: 5px;
margin-left: 0;
font-size: 12px;
color: #808080;
}
</style>
{% endblock %}
{% block title %}
{{ site_obj.site_title }}
{% endblock %}
{% block content %}
<div class="col-md-2">
{% load mytag %}
{% mymenu username %}
</div>
<div class="col-md-10">
<h2 class="text-center"> {{ article_obj.title }}</h2>
{{ article_obj.content|safe }}
<!--文章点赞点踩样式开始-->
<div class="clearfix">
<div id="div_digg">
<div class="diggit upordown">
<span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span>
</div>
<div class="buryit upordown">
<span class="burynum" id="bury_count">{{ article_obj.down_num }}</span>
</div>
<div class="clear"></div>
<span style="color:red" id="d1"> </span>
<div class="diggword" id="digg_tips"></div>
</div>
</div>
<!--文章点赞点踩样式结束-->
{# 文章评论楼的渲染开始#}
<div class="comment_list">
<ul class="list-group">
{% for comment_obj in comment_list %}
<li class="list-group-item" >
<span><a href="">#{{ forloop.counter }}楼</a></span>
<span>{{ comment_obj.comment_time|date:'H:i' }}</span>
<span><a href="/{{ comment_obj.user.username }}/">{{ comment_obj.user.username }}</a></span>
<p class="pull-right"><a href="#">引用 </a></p>
<p class="pull-right"><a href="#" class="reply" comment_id="{{ comment_obj.pk }}" username="{{ comment_obj.user.username }}">回复 </a></p>
<p>
{% if comment_obj.parent_id %}
@{{ comment_obj.parent.user.username }}
{% endif %}
</p>
<p>
{{ comment_obj.content }}
</p>
</li>
{% endfor %}
</ul>
</div>
{# 文章评论楼的渲染结束#}
{# 文章评论样式开始#}
{% if request.user.is_authenticated %}
<div class="comment_area">
<p><span class="glyphicon glyphicon-comment"></span>发表评论</p>
<textarea name="" id="comment" cols="30" rows="5" class="form-control"></textarea>
<button class="btn btn-warning" id="commentBtn">提交评论</button>
</div>
{% else %}
<p>
<a href="/register/">注册</a>
<a href="/login/">登录</a>
</p>
{% endif %}
{# 文章评论样式结束#}
</div>
{% endblock %}
{% block js %}
<script>
//给点赞点踩图标绑定点击事件
$('.upordown').click(function () {
let currentEle = $(this);
let isUp = $(this).hasClass('diggit') //判断标签是否含有某个class值 从而二选一区分赞和踩
// 发送ajax请求
$.ajax({
url: '/up_or_down/', // 点赞点踩有一定的逻辑 单独开设接口处理
type: 'post',
data: {
'csrfmiddlewaretoken': '{{ csrf_token }}',
'article_pk': '{{ article_obj.pk }}',
'is_up': isUp,
},
success:function (args) {
if (args.code === 10000){
{#currentEle.find('span').text(Number(currentEle.find('span').text()) + 1)#}
// 找一下span标签 用number转数字
{#currentEle.children().first().text(Number(currentEle.find('span').text()) + 1)#}
currentEle.children().first().text(Number(currentEle.children().first().text()) + 1)
}
$('#d1').html(args.msg)
}
})
})
// 提前创建一个全局变量用于存储评论主键值
let parentId = null;
// 给提交评论的按钮绑定点击事件
$('#commentBtn').click(function () {
// 获取用户评论的内容
let commentMsg = $('#comment').val();
let currentUserName= '{{ request.user.username }}';
let oldCommentMsg = commentMsg;
// 如果发送的是子评论 那么需要处理掉前缀内容(前端可以做 后端也可以做)
if(parentId){
commentMsg = commentMsg.slice(commentMsg.indexOf('\n')+1)
}
$.ajax({
url:'/comment/',
type:'post',
data:{
'csrfmiddlewaretoken': '{{ csrf_token }}',
'article_pk': '{{ article_obj.pk }}',
'content': commentMsg,
'parent_id':parentId,
},
success:function (args) {
if (args.code === 10000){
// 清空评论框内容
$('#comment').val('');
// 动态创建标签 并添加到评论楼中
let tempComment = `
<li class="list-group-item" >
<span class="glyphicon glyphicon-comment"><a href="/${currentUserName}/">${currentUserName}</a></span>
<p>
${oldCommentMsg}
</p>
</li>
`
// 查找ul标签 然后添加上述的标签内容即可
$('.list-group').append(tempComment)
// 清空全局变量
parentId = null;
}
}
})
})
// 给回复按钮绑定点击事件
$('.reply').click(function () {
// 获取回复按钮所在的评论用户名
let targetUserName = $(this).attr('username');
// 获取回复按钮所在的评论主键值 修改全局变量
parentId = $(this).attr('comment_id')
$('#comment').val('@'+ targetUserName + '\n').focus();
})
</script>
{% endblock %}
个人站点、点赞点踩及文章评论后端代码
def article_detail_func(request, username, article_id):
# 筛选某篇具体的文章对象
article_obj = models.Article.objects.filter(site__site_name=username).filter(pk=article_id).first()
site_obj = models.Site.objects.filter(site_name=username).first()
'''这里也可以添加健壮性校验 防止用户自己瞎传数据'''
# 获取当前文章所有的评论数据
comment_list = models.Comment.objects.filter(article=article_obj)
return render(request, 'articleDetailPage.html', locals())
def up_or_down_func(request):
print(request.POST)
"""
1.校验用户是否登录
2.校验当前文章是否是当前用户自己的
3.校验当前文章是否已经被当前用户点过
4.创建点赞点踩记录(不要忘记文章表中的优化字段 同步自增)
"""
back_dict = {'code': 10000, 'msg': ''}
if request.method == 'POST':
'''判断用户是否登录'''
if request.user.is_authenticated:
article_pk = request.POST.get('article_pk')
is_up = request.POST.get('is_up') # true 普通的字符串
article_obj = models.Article.objects.filter(pk=article_pk).first()
# 校验当前文章是否是当前用户自己的
if not article_obj.site.userinfo == request.user:
is_click = models.UpAndDown.objects.filter(user=request.user, article=article_obj)
if not is_click:
is_up = json.loads(is_up) # 自动转换成python中的布尔值
print(is_up, type(is_up))
if is_up:
models.Article.objects.filter(pk=article_pk).update(up_num=F('up_num') + 1)
back_dict['msg'] = '点赞成功'
else:
models.Article.objects.filter(pk=article_pk).update(down_num=F('down_num') + 1)
back_dict['msg'] = '点踩成功'
models.UpAndDown.objects.create(user=request.user, article=article_obj, is_up=is_up)
else:
back_dict['code'] = 10001
back_dict['msg'] = '您已经点过了'
else:
back_dict['code'] = 10002
back_dict['msg'] = '你个臭不要脸 不能给自己点'
else:
back_dict['code'] = 10003
# from django.utils.safestring import mark_safe
# back_dict['msg'] = mark_safe('请先<a herf="/login/">登录</a>')
back_dict['msg'] = '请先<a href="/login/">登录</a>'
print(back_dict)
return JsonResponse(back_dict)
@login_required
def comment_func(request):
back_dict = {'code': 10000, 'msg': ''}
if request.method == 'POST':
article_pk = request.POST.get('article_pk')
content = request.POST.get('content')
parent_id = request.POST.get('parent_id') # 直接获取即可 无需关心是否有值
models.Article.objects.filter(pk=article_pk).update(comment_num=F('comment_num') + 1)
models.Comment.objects.create(user=request.user, article_id=article_pk, content=content, parent_id=parent_id)
back_dict['msg'] = '评论成功'
return JsonResponse(back_dict)
后台管理各个页面的搭建
1.在templates下面创建一个backend的文件夹用来放置后台处理的各个页面
2.开始后台管理页面的路由,一个路由对应一个页面
# 后台管理接口
path('backend/', views.backend_func),
添加文章前端页面需要引入kindeditor文件 可视化html编辑器
# 后台管理之添加文章接口
path('add_article/', views.add_article_func),
# 后台管理之文章上传文件
path('upload_img/', views.upload_img_func),
# 后台管理之编辑文章功能
path('edit_article/<int:article_pk>/', views.edit_article_func),
# 后台管理之文章删除
path('delete_article/', views.delete_article_func),
后台管理功能前端代码
backendBasePage.html代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
{% load static %}
<link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}">
<script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script>
{% block css %}
{% endblock %}
</head>
<body>
<!--导航条开始-->
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">
{% block title %}
BBS
{% endblock %}
</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="#">博客 <span class="sr-only">(current)</span></a></li>
<li><a href="#">文章</a></li>
</ul>
<form class="navbar-form navbar-left">
<div class="form-group">
<input type="text" class="form-control" placeholder="搜索">
</div>
<button type="submit" class="btn btn-default">搜索</button>
</form>
<ul class="nav navbar-nav navbar-right">
{% if request.user.is_authenticated %}
<li><a href="#">{{ request.user.username }}</a></li>
<li style="width: 38px;height: 38px;border-radius: 50%; overflow: hidden;display: block"><img src="/media/{{ request.user.avatar }}/" alt="" style="max-width: 100%"></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
aria-expanded="false">更多操作 <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#" data-toggle="modal" data-target="#myModal">修改密码</a></li>
<li><a href="#" data-toggle="modal" data-target="#myavatar">修改头像</a></li>
<li role="separator" class="divider"></li>
<li><a href="/logout/">注销登录</a></li>
</ul>
</li>
{% else %}
<li><a href="{% url 'register_view' %}">注册</a></li>
<li><a href="{% url 'login_view' %}">登录</a></li>
{% endif %}
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
<!--导航条结束-->
<!--模态框开始-->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span>
</button>
<h4 class="modal-title text-center" id="myModalLabel">修改密码</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label for="">用户名</label>
<input type="text" value="{{ request.user.username }}" disabled class="form-control">
</div>
<div class="form-group">
<label for="">原密码</label>
<input type="text" id="old_pwd" class="form-control">
</div>
<div class="form-group">
<label for="">新密码</label>
<input type="text" id="new_pwd" class="form-control">
</div>
<div class="form-group">
<label for="">确认密码</label>
<input type="text" id="confirm_pwd" class="form-control">
</div>
</div>
<div class="modal-footer">
<span id="error" style="color: red" ></span>
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-warning" id="setBtn">修改</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="myavatar" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span>
</button>
<h4 class="modal-title text-center" id="myModalLabel">修改头像</h4>
</div>
<div class="modal-body">
<form action="/set_avatar/" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-group">
<label for="">用户名</label>
<input type="text" value="{{ request.user.username }}" disabled class="form-control">
</div>
<div class="form-group">
<label for="">原头像</label>
<img src="/media/{{ request.user.avatar }}" alt="" width="120">
</div>
<div class="form-group">
<label for="myfile">新头像
<img src="/static/img/default.jpg" alt="" id="myimg" width="120">
</label>
<input type="file" id="myfile" style="display: none" name="new_avatar" >
</div>
<input type="submit" class="btn btn-warning btn-block">
</form>
</div>
</div>
</div>
</div>
<!--模态框结束-->
<!--内容区开始-->
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingOne">
<h4 class="panel-title">
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseOne"
aria-expanded="true" aria-controls="collapseOne">
博客后台
</a>
</h4>
</div>
<div id="collapseOne" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingOne">
<div class="panel-body">
<p><a href="/add_article/">添加文章</a></p>
<p><a href="">添加分类</a></p>
<p><a href="">添加标签</a></p>
<p><a href="">更多操作</a></p>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingTwo">
<h4 class="panel-title">
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion"
href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
四四四四丝丝
</a>
</h4>
</div>
<div id="collapseTwo" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingTwo">
<div class="panel-body">
Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid.
3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt
laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin
coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes
anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings
occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard
of them accusamus labore sustainable VHS.
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingThree">
<h4 class="panel-title">
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion"
href="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
友情链接
</a>
</h4>
</div>
<div id="collapseThree" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingThree">
<div class="panel-body">
Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid.
3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt
laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin
coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes
anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings
occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard
of them accusamus labore sustainable VHS.
</div>
</div>
</div>
</div>
</div>
{% block content %}
<div class="col-md-10">
<div>
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#home" aria-controls="home" role="tab" data-toggle="tab">文章</a></li>
<li role="presentation"><a href="#profile" aria-controls="profile" role="tab" data-toggle="tab">随笔</a></li>
<li role="presentation"><a href="#messages" aria-controls="messages" role="tab" data-toggle="tab">日记</a></li>
<li role="presentation"><a href="#settings" aria-controls="settings" role="tab" data-toggle="tab">配置</a></li>
<li role="presentation"><a href="#see" aria-controls="settings" role="tab" data-toggle="tab">你瞅瞅你see you</a></li>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="home">
{% block articleConetent %}
{% endblock %}
</div>
<div role="tabpanel" class="tab-pane" id="profile">随笔列表</div>
<div role="tabpanel" class="tab-pane" id="messages">日记列表</div>
<div role="tabpanel" class="tab-pane" id="settings">配置列表</div>
<div role="tabpanel" class="tab-pane" id="see">你瞅瞅你列表</div>
</div>
</div>
</div>
{% endblock %}
</div>
</div>
<!--内容区结束-->
<script>
$('#setBtn').click(function () {
$.ajax({
url: '/set_pwd/',
type: 'post',
data: {
'old_pwd': $('#old_pwd').val(),
'new_pwd': $('#new_pwd').val(),
'confirm_pwd': $('#confirm_pwd').val(),
'csrfmiddlewaretoken': '{{ csrf_token }}',
},
success: function (args) {
if(args.code === 10000){
window.location.href = args.url
}else{
$('#error').text(args.msg)
}
}
})
})
//1.用户头像的实时展示
$('#myfile').change(function (){
//1.产生一个文件阅读器对象
let myFileReaderObj = new FileReader();
// 2.获取用户上传的头像文件
let fileObj = this.files[0];
//3.将文件对象交给阅读器对象读取
myFileReaderObj.readAsDataURL(fileObj); //异步
// 等待文件阅读器对象加载完毕之后再修改src
myFileReaderObj.onload = function (){
//4.修改img标签的src 属性展示图片
$('#myimg').attr('src',myFileReaderObj.result)
}
})
</script>
{% block js %}
{% endblock %}
</body>
</html>
backend.html代码
{% extends 'backend/backendBasePage.html' %}
{% block css %}
<link rel="stylesheet" href="media/css/{{ site_obj.site_theme }}/">
{% endblock %}
{% block title %}
{{ site_obj.site_title }}
{% endblock %}
{% block articleConetent %}
<table class="table table-hover table-striped">
<thead>
<tr>
<th>标题</th>
<th>时间</th>
<th>评论数</th>
<th>点赞数</th>
<th>点踩数</th>
<th>操作</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for article_obj in page_queryset %}
<tr>
<td><a href="/{{ site_obj.site_name }}/article/{{ article_obj.pk }}">{{ article_obj.title }}</a></td>
<td>{{ article_obj.create_time|date:'Y-m-d H:i:s' }}</td>
<td>{{ article_obj.comment_num }}</td>
<td>{{ article_obj.up_num }}</td>
<td>{{ article_obj.down_num }}</td>
<td><a href="/edit_article/{{ article_obj.pk }}/">编辑</a></td>
<td><a href="#" class="delBtn" article_pk="{{ article_obj.pk }}">删除</a></td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pull-right">{{ page_obj.page_html|safe }} </div>
{% endblock %}
{% block js %}
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
<script>
$('.delBtn').click(function () {
let currentBtn = $(this);
swal({
title: "你确定要删除吗?",
text: "删除了 后果自负!!!",
icon: "warning",
buttons: ["取消", "确认"],
dangerMode: true,
})
.then((willDelete) => {
if (willDelete) {
// 用户点击ok按钮 发送ajax请求
$.ajax({
url:'/delete_article/',
type:'post',
data:{'csrfmiddlewaretoken':'{{ csrf_token }}','article_pk':$(this).attr('article_pk')},
success:function (args) {
if(args.code===10000){
swal(args.msg, {
icon: "success",
});
// 使用DOM操作 临时刷新页面
currentBtn.parent().parent().remove()
}else{
swal(args.msg);
}
}
})
} else {
swal("你个怂货 数据都不敢删!");
}
});
})
</script>
{% endblock %}
addArticlePage.html代码
{% extends 'backend/backendBasePage.html' %}
{% block css %}
<link rel="stylesheet" href="media/css/{{ site_obj.site_theme }}/">
{% endblock %}
{% block title %}
{{ site_obj.site_title }}
{% endblock %}
{% block articleConetent %}
<h2 class="text-center">添加文章</h2>
<form action="" method="post">
{% csrf_token %}
<p>文章标题</p>
<input type="text" name="title" class="form-control">
<p>文章内容</p>
<textarea name="content" id="mycontent" cols="30" rows="10" class="form-control"></textarea>
<p>文章分类</p>
<p>
{% for category_obj in category_list %}
<input type="radio" name="category" value="{{ category_obj.pk }}">{{ category_obj.name }}
{% endfor %}
</p>
<p>文章标签</p>
<p>
{% for tag_obj in tag_list %}
<input type="checkbox" name="tag" value="{{ tag_obj.pk }}">{{ tag_obj.name }}
{% endfor %}
</p>
<input type="submit" class="form-control btn btn-success btn-block" value="发布" >
</form>
{% endblock %}
{% block js %}
<script charset="utf-8" src="/static/kindeditor/kindeditor-all-min.js"></script>
<script charset="utf-8" src="/static/kindeditor/lang/zh-CN.js"></script>
<script>
KindEditor.ready(function(K) {
window.editor = K.create('#mycontent',
{
width : '100%',
height: '600px',
resizeType:1,
uploadJson:'/upload_img/',
extraFileUploadParams : {
csrfmiddlewaretoken : '{{ csrf_token }}',
},
});
});
</script>
{% endblock %}
editArticlePage.html代码
{% extends 'backend/backendBasePage.html' %}
{% block css %}
<link rel="stylesheet" href="media/css/{{ site_obj.site_theme }}/">
{% endblock %}
{% block title %}
{{ site_obj.site_title }}
{% endblock %}
{% block articleConetent %}
<h2 class="text-center">添加文章</h2>
<form action="" method="post">
{% csrf_token %}
<p>文章标题</p>
<input type="text" name="title" class="form-control" value="{{ article_obj.title }}">
<p>文章内容</p>
<textarea name="content" id="mycontent" cols="30" rows="10" class="form-control" >{{ article_obj.content }}</textarea>
<p>文章分类</p>
<p>
{% for category_obj in category_list %}
{% if article_obj.category == category_obj %}
<input type="radio" name="category" checked value="{{ category_obj.pk }}">{{ category_obj.name }}
{% else %}
<input type="radio" name="category" value="{{ category_obj.pk }}">{{ category_obj.name }}
{% endif %}
{% endfor %}
</p>
<p>文章标签</p>
<p>
{% for tag_obj in tag_list %}
{% if tag_obj in article_obj.tags.all %}
<input type="checkbox" checked name="tag" value="{{ tag_obj.pk }}">{{ tag_obj.name }}
{% else %}
<input type="checkbox" name="tag" value="{{ tag_obj.pk }}">{{ tag_obj.name }}
{% endif %}
{% endfor %}
</p>
<input type="submit" class="form-control btn btn-danger btn-block" value="发布" >
</form>
{% endblock %}
{% block js %}
<script charset="utf-8" src="/static/kindeditor/kindeditor-all-min.js"></script>
<script charset="utf-8" src="/static/kindeditor/lang/zh-CN.js"></script>
<script>
KindEditor.ready(function(K) {
window.editor = K.create('#mycontent',
{
width : '100%',
height: '600px',
resizeType:1,
uploadJson:'/upload_img/',
extraFileUploadParams : {
csrfmiddlewaretoken : '{{ csrf_token }}',
},
});
});
</script>
{% endblock %}
后台管理后端代码
from django.shortcuts import render, HttpResponse, redirect
from django.contrib.auth.decorators import login_required
@login_required
def backend_func(request):
site_obj = models.Site.objects.filter(site_name=request.user.username).first()
# 获取当前站点下所有的文章
article_queryset = models.Article.objects.filter(site=site_obj)
page_obj = mypage.Pagination(current_page=request.GET.get('page'), all_count=article_queryset.count())
page_queryset = article_queryset[page_obj.start:page_obj.end]
return render(request, 'backend/backend.html', locals())
from bs4 import BeautifulSoup
@login_required
def add_article_func(request):
site_obj = models.Site.objects.filter(site_name=request.user.username).first()
if request.method == 'POST':
title = request.POST.get('title')
content = request.POST.get('content')
category_id = request.POST.get('category')
tag_list = request.POST.getlist('tag')
# 讲文章内容交给bs4模块处理
soup = BeautifulSoup(content, 'lxml') # 第二个参数是解析器 不同的解析器功能不一样 最好是lxml
tags = soup.find_all()
for tag in tags:
if tag.name == 'script':
tag.decompose() # 删除script标签
article_obj = models.Article.objects.create(
title=title,
# desc=content[0:150], # 简单处理
desc=soup.text[0:150], # 获取文本然后切割150个字符
content=str(soup), # 使用处理之后的内容(不包含script标签)
# content=content,
site=site_obj,
category_id=category_id,
)
# 自己去操作文章和标签的第三张表 无法使用add set remove clear
# for tag_id in tag_list: # 当tag_list较多的适合 频繁执行create速度会满 >>> 大批量添加数据bulk_create, bulk_update(更新数据)
# models.Article2Tag.objects.create(article=article_obj, tag_id=tag_id)
# 下面这个方法效率更高
tag_obj_list = []
for tag_id in tag_list:
article2tag_obj = models.Article2Tag(article=article_obj, tag_id=tag_id)
tag_obj_list.append(article2tag_obj)
models.Article2Tag.objects.bulk_create(tag_obj_list) # 可以用列表生成式
return redirect('/backend/')
category_list = models.Category.objects.filter(site=site_obj)
tag_list = models.Tag.objects.filter(site=site_obj)
return render(request, 'backend/addArticlePage.html', locals())
from django.conf import settings
import os
@login_required
def upload_img_func(request):
# 返回的数据格式也是有要求的
"""
{
"error" : 0,
"url" : "http://www.example.com/path/to/file.ext"
}
//失败时
{
"error" : 1,
"message" : "错误信息"
}
"""
back_dict = {
"error": 0,
}
# 获取编辑器上传的图片数据
img_obj = request.FILES.get('imgFile')
# 拼接文章图片所存放的路径
img_dir_path = os.path.join(settings.BASE_DIR, 'media', 'article')
if not os.path.exists(img_dir_path):
os.mkdir(img_dir_path)
img_file_path = os.path.join(img_dir_path, f'{img_obj.name}') # 针对文件名 防止冲突添加唯一标识 比如:用户名+当前时间+文件名
with open(img_file_path, 'wb') as f:
for line in img_obj:
f.write(line)
back_dict['url'] = 'media/article/%s' % img_obj.name # 不能返回后端绝对路径而应该是暴露的接口路径
return JsonResponse(back_dict)
@login_required
def edit_article_func(request, article_pk):
site_obj = models.Site.objects.filter(site_name=request.user.username).first()
article_obj = models.Article.objects.filter(pk=article_pk).first()
if request.method == 'POST':
title = request.POST.get('title')
content = request.POST.get('content')
category_id = request.POST.get('category')
tag_list = request.POST.getlist('tag')
# 讲文章内容交给bs4模块处理
soup = BeautifulSoup(content, 'lxml') # 第二个参数是解析器 不同的解析器功能不一样 最好是lxml
tags = soup.find_all()
for tag in tags:
if tag.name == 'script':
tag.decompose() # 删除script标签
models.Article.objects.filter(pk=article_pk).update(
title=title,
desc=soup.text[0:150], # 获取文本然后切割150个字符
content=str(soup), # 使用处理之后的内容(不包含script标签)
site=site_obj,
category_id=category_id,
)
models.Article2Tag.objects.filter(article_id=article_pk).delete()
tag_obj_list = []
for tag_id in tag_list:
article2tag_obj = models.Article2Tag(article=article_obj, tag_id=tag_id)
tag_obj_list.append(article2tag_obj)
models.Article2Tag.objects.bulk_create(tag_obj_list) # 可以用列表生成式
return redirect('/backend/')
category_list = models.Category.objects.filter(site=site_obj)
tag_list = models.Tag.objects.filter(site=site_obj)
return render(request, 'backend/editArticlePage.html', locals())
@login_required
def delete_article_func(request):
back_dict = {'code': 10000, 'msg': ''}
if request.method == 'POST':
article_pk = request.POST.get('article_pk')
models.Article.objects.filter(pk=article_pk).delete()
models.Article2Tag.objects.filter(article_id=article_pk).delete()
back_dict['msg'] = '删除成功 准备跑路!'
return JsonResponse(back_dict)
@login_required
def set_avatar_func(request):
new_avatar = request.FILES.get('new_avatar')
# update 不会自动加avatar前缀
# models.UserInfo.objects.filter(pk=request.user.pk).update(avatar=new_avatar)
user_obj = models. UserInfo.objects.filter(pk=request.user.pk).first()
user_obj.avatar = new_avatar
user_obj.save() # 保存数据
return redirect('/backend/')