BBS-后台管理
目录
后台管理功能是:让用户自己去操作增删改查。新建一个应用(app02),要记得在settings中注册应用。
本篇文章只写文章列表、添加文章、删除文章。自己去写编辑文章,对标签的增删改查、对分类的增删改查。只是表不同,字段不同。逻辑是一样的。
重点功能:
- 富文本编辑器使用
- 新增文章(防止XSS攻击)
- 文章新增时,上传图片
一、添加路由
路由分发,urls.py
from django.conf.urls import url,include
# 路由分发
url(r'^app02/', include('app02.urls')), # 后面不能加$符号
app02下的urls.py
from django.conf.urls import url
from django.contrib import admin
from app02 import views
urlpatterns = [
url(r'^admin/', admin.site.urls),
# 文章列表路由
url(r'^article_list/', views.article_list),
# 添加文章路由
url(r'^article/add/', views.add),
# 删除路由
url(r'^article/delete/', views.delete),
# 上传图片的路由
url(r'^upload/', views.upload),
]
二、后台管理前端模板
文章列表展示
前端逻辑:
1.table标签
2.展示文章数据
文章标题,路由跳转:用户名/article/文章id,target="_blank"表示新打开个标签页
文章点赞数、点踩数、评论数
文章分类,文章查分类,正向
注册时间,格式化
编辑按钮
删除按钮,阻止自己的事件
3.删除绑定事件
删除绑定事件
3.1 获取主键id,因为每一条的主键id都不一样,就给删除添加一个自定义属性来保存循环中的文章id
3.2 二次确认,layer询问框
3.3 发起ajax请求
3.4 删除成功后,删除当前的tr
文章列表展示前端代码:
backend文件夹下的article_list.html页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>展示文章列表</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<div class="row">
<h1 class="text-center">文章列表</h1>
<!-- 添加文章标签 -->
<a href="/app02/article/add/" class="btn btn-info">添加文章</a>
<table class="table table-striped table-hover">
<thead>
<tr><!--需要展示的字段-->
<th>标题</th>
<th>点赞数</th>
<th>点踩数</th>
<th>评论数</th>
<th>分类</th>
<th>添加时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for article in article_list %}<!-- 展示文章数据 -->
<tr>
<!-- 文章详情页的路由:用户名/article/文章id,target="_blank"表示新打开个标签页 -->
<td><a href="/{{ user_obj.username }}/article/{{ article.pk }}"
target="_blank">{{ article.title }}</a></td>
<td>{{ article.up_num }}</td>
<td>{{ article.down_num }}</td>
<td>{{ article.comment_num }}</td>
<td>{{ article.category.name }}</td><!-- 文章查分类,正向 -->
<td>{{ article.create_time|date:'Y-m-d' }}</td>
<td>
<a href="" class="btn btn-success">编辑</a>
<a href="javascript:;" class="btn btn-danger del" pk="{{ article.pk }}">删除</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<script src="/static/layer/layer.js"></script>
<script>
{# 删除绑定点击事件开始 #}
$(".del").click(function () {
var _this = $(this)
// 1. 主键id怎么获取?
// 因为每一条的主键id都不一样,就给删除添加一个自定义属性来保存循环中的文章id
var id = $(this).attr('pk');
// 2. 二次确认,询问框
layer.confirm('您确认要删除这条记录吗?', {
btn: ['确认'] //按钮
}, function () {
// 3. 发起ajax请求
$.ajax({
url: '/app02/article/delete/', // 删除路由
type: 'post',
data: {
id: id,
csrfmiddlewaretoken: '{{ csrf_token }}'
},
success: function (res) {
// 删除当前的tr,DOM操作
// 只要一成功,就先删除数据,再返回信息
_this.parent().parent().remove();
if (res.code == 200) {
layer.msg(res.msg, {},function(){
{#location.reload();刷新页面#}
// _this.parent().parent().remove(); 这个语句写在函数中,反应比较慢,为什么?function是回调函数,需要后端处理完逻辑并返回数据之后,才会调用,所以速度会慢一些。
})
} else {
layer.msg(res.msg)
}
}
})
});
})
{# 删除绑定点击事件结束 #}
</script>
</body>
</html>
添加文章
前端逻辑:
1.搭建样式
2.文章内容使用textarea文本框,使用富文本编辑器
3.文章分类使用下拉框,循环分类列表
4.文章标签使用复选框,定义name、value属性
5.富文本编辑器,直接看对应文档使用
6.提交按钮绑定点击事件
6.1 获取参数
文章标题
文章内容,看文档学习使用方法
分类
标签的复选框,获取数据(input框[属性选择器]:选中的值),each循环取值,添加到数组中,数组要用js转成字符串
6.2 验证参数
6.3 发起ajax请求
6.4 添加成功后,跳转到文章列表页
富文本编辑器
文章内容使用富文本编辑器
富文本编辑器介绍:https://blog.csdn.net/q5926167/article/details/127445887)
kindeditor文本编辑器官网:http://kindeditor.net/demo.php
文本编辑器如何使用
1.看文档
2.下载,官方下载
3.编辑器的使用方法
部署编辑器:把下载的所有文件直接复制到项目中
修改HTML页面:先有textarea文本框,引入kindeditor.js
获取HTML数据
4.编辑器初始化参数
K.create()中的第二个参数,是对象形式
width
hright
items:[],工具栏
resizeType: 1,
5.上传文件
uploadJson
额外参数,添加csrf_token参数
返回格式json
具体使用:
<textarea name="" id="editor_id" cols="30" rows="10" class="form-control"></textarea>
<script charset="utf-8" src="/static/kindeditor/kindeditor-all.js"></script> // 改成自己的路径
<script charset="utf-8" src="/static/kindeditor/lang/zh-CN.js"></script>
<script>
KindEditor.ready(function (K) {
window.editor = K.create('#editor_id', // 警号后的名字要跟taxtarea中的id属性值一致
{width: '900px',
height: '500px',
items: ['source', '|', 'image', 'multiimage'],
resizeType: 1,
uploadJson: '/app02/upload/', // 上传图片的路由地址
extraFileUploadParams: {
csrfmiddlewaretoken: '{{ csrf_token }}',
}
});
});
</script>
前端add.html页面代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>添加文章页面</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<div class="row">
<h1 class="text-center">添加文章页面</h1>
<div class="form-group">
<label for="title">文章标题</label>
<input type="text" class="form-control" id="title">
</div>
<div class="form-group">
<label for="username">文章内容</label><!-- 文章内容用文本框 -->
<textarea name="" id="editor_id" cols="30" rows="10" class="form-control"></textarea>
</div>
<div class="form-group">
<label for="category">文章分类</label><!-- 文章分类使用下拉框 -->
<select name="" id="category" class="form-control">
{% for category in category_list %}<!-- option中写分类名称,value值写分类id -->
<option value="{{ category.pk }}">{{ category.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="">文章标签</label><!-- 文章标签使用复选框 -->
{% for tag in tag_list %}<!-- 复选框不能使用id,一循环id会重复 -->
<input type="checkbox" name="tag" value="{{ tag.pk }}"> {{ tag.name }}
{% endfor %}
</div>
<input type="button" class="btn btn-success commit" value="提交">
</div>
</div>
<script charset="utf-8" src="/static/kindeditor/kindeditor-all.js"></script> // 改成自己的路径
<script charset="utf-8" src="/static/kindeditor/lang/zh-CN.js"></script>
<script src="/static/layer/layer.js"></script>
<script>
{# 富文本编辑器使用开始 #}
KindEditor.ready(function (K) {
window.editor = K.create('#editor_id', // 警号后的名字要跟taxtarea中的id属性值一致
{
width: '900px',
height: '500px',
items: [
'source', '|', '|', 'preview', 'print',
'copy', '|', 'image', 'multiimage',], // 工具栏
resizeType: 1,
uploadJson: '/app02/upload/', // 上传图片的路由地址
extraFileUploadParams: { // 上传图片额外参数,csrf_token参数
csrfmiddlewaretoken: '{{ csrf_token }}',
}
});
});
{# 富文本编辑器使用结束 #}
{# 给提交按钮绑定点击事件开始 #}
$(".commit").click(function () {
editor.sync();
// 1. 获取参数
var title = $("#title").val();
// 文章内容,看文档使用它的获取数据的方法
var content = $('#editor_id').val(); // 取得HTML内容
{#html = editor.html();#}
var category = $("#category").val(); // 分类
// 标签,复选框,获取数据的特殊方式:input框[属性选择器]:选中的值
// var tags = $("input[name='tag']:checked") // tags是jquery对象
// each循环取值
var tag_arr = [] // 定义一个数组,把每个值加到数组中
$.each($("input[name='tag']:checked"), function (index, value) {
// $(this)是每一个选中的标签对象(回调函数前面的对象)
// $(this).val()jquery对象获取value值
// push()数组尾部追加元素
tag_arr.push($(this).val());
});
// console.log(tag_arr) // (3) ['1', '2', '3']
// 2. 验证参数
// js数组转为字符串 [1,2,3],数组提交不方便
var tag_str = tag_arr.join(',');
// console.log(tag_str)
// 3. 发起ajax请求
$.ajax({
url: '',
type: 'post',
data: {
title: title,
content: content,
category: category,
tag_str: tag_str, // 字符串形式
csrfmiddlewaretoken: '{{ csrf_token }}'
},
success: function (res) {
if (res.code == 200) {
layer.msg(res.msg, {}, function () {
// 添加成功,跳转到文章列表页
location.href = '/app02/article_list/'
})
} else {
layer.msg(res.msg)
}
}
})
})
{# 给提交按钮绑定点击事件结束 #}
</script>
</body>
</html>
二、视图函数
文章列表展示
app02下的views.py:
from app01 import models # 导入app01中的模型类
# from django.contrib.auth.decorators import login_required
# @login_required(login_url='/login/') # 使用django的auth模块,但是这个不能使用,我们重新写的加密方式改变了。密码不可能一致
def article_list(request):
if not request.session.get('username'): # 不登陆不能访问,也可以加个装饰器
return redirect('/login/')
# 1. 根据用户id查询出用户对象
user_obj = models.UserInfo.objects.filter(pk=request.session.get('id')).first()
# 2.查询当前站点下的文章列表
# filter(blog(外键)=站点对象)
# filter(blog_id(外键)=站点对象点字段)
article_list = models.Article.objects.filter(blog=user_obj.blog).all()
return render(request, 'backend/article_list.html', locals())
添加文章
后端逻辑
# 前端需要展示的数据(1-3步)
1. 先查询出当前站点,根据用户表中查询外键blog_id
2. 查询当前站点下的所有文章分类
3. 查询当前站点下的所有标签
# 接收前端的ajax请求
1. 定义返回数据的格式
2. 接受参数
标签字符串需要转成列表形式
区分文本中的标签和中文文本内容,要借助于一个模块bs4
删除script标签(tag.name)
摘要,截取中文文本soup.text[0:100]
3. 验证参数
4. 处理正常的业务逻辑,操作两张表:1.文章表,2.文章和标签的第三章表
文章表是插入数据
第三章表,插入数据,标签可能是多个需要循环
5.返回给前端数据
"""
容易出错的两个方面:
1.对于摘要,要识别标签,只截取文本
2.把script标签去除(XSS攻击)
"""
BeautifulSoup4
BeautifulSoup也是一个HTML/XML的解析器,主要的功能也是如何解析和提取HTML/XML数据。
中文官网:https://www.crummy.com/software/BeautifulSoup/bs4/doc.zh/
英文官网:
https://www.crummy.com/software/BeautifulSoup/bs4/doc/
具体使用:https://baijiahao.baidu.com/s?id=1663904567728406423&wfr=spider&for=pc
添加文章后端代码
from django.http import JsonResponse
def add(request):
"""添加文章"""
"""
容易出错的两个方面:
1.对于摘要,要识别标签,只截取文本
2.把script标签去除(XSS攻击)
"""
if not request.session.get('username'): # 不登陆不能够访问。先做简单的判断
return redirect('/login/')
# 1. 先查询出当前站点,根据用户表中查询外键blog_id
user_obj = models.UserInfo.objects.filter(pk=request.session.get('id')).first()
blog = user_obj.blog
if request.method == 'POST': # 接收前端的ajax请求
# 1. 定义返回数据的格式
back_dic = {'code': 200, 'msg': '添加成功'}
# 2. 接受参数
title = request.POST.get("title")
content = request.POST.get("content")
category_id = request.POST.get("category")
tag_str = request.POST.get("tag_str") # string----->1,2,3
# 把字符串转成列表形式
tags_arr = tag_str.split(',')
print(tags_arr)
'''区分文本中的标签和中文文本内容,要借助于一个模块bs4:
必须安装:pip install beautifulsoup4
'''
from bs4 import BeautifulSoup
# BeautifulSoup(文本数据变量, '解析器')
soup = BeautifulSoup(content, 'html.parser')
# BeautifulSoup(content, 'lxml') # 必须安装lxml pip install lxml
print(soup, type(soup))
# 把script标签去除(XSS攻击)
# 方法:soup.find_all('') # 查找出所有标签 # 支持for循环的
for tag in soup.find_all():
# print(tag.name) # 标签名称
'''xss攻击的原理:前端提交的带有script标签的js代码,我们只需要把提交过来的script标签注释掉或者删除掉'''
if tag.name == 'script':
# 删除script标签
tag.decompose()
# 方式二:
# for tag in soup.find_all('script'): # 只找script标签,就不用判断了
# tag.decompose()
# 摘要,截取中文字符
# desc = content[0:100] # 截取的是文章内容的前100个字符。会截到标签
desc = soup.text[0:100] # soup.text是中文文本,然后直接截取就行
# 3. 参数验证省略
# 4. 处理正常的业务逻辑
# 操作两张表:1.文章表,2.文章和标签的第三章表
# 文章表是插入数据
# article_obj:当前插入数据成功的对象
article_obj = models.Article.objects.create(title=title,
desc=desc,
content=str(soup), # 需要转成字符串形式
category_id=category_id,
blog=blog
)
# 操作第三章表,插入数据,标签可能是多个需要循环
for i in tags_arr:
# 这种方式操作数据库次数比较多
models.Article2Tag.objects.create(article_id=article_obj.pk, tag_id=i)
# 优化一下:批量插入
# tag_obj = []
# for i in tags_arr:
# obj = models.Article2Tag(article_id=article_obj.pk, tag_id=i)
# tag_obj.append(obj)
# models.Article2Tag.objects.bulk_create(tag_obj)
# 5.返回给前端数据
return JsonResponse(back_dic)
# 2. 查询当前站点下的所有文章分类
category_list = models.Category.objects.filter(blog=blog).all()
# 3. 查询当前站点下的所有标签
tag_list = models.Tag.objects.filter(blog=blog).all()
return render(request, 'backend/add.html', locals())
删除文章
代码:
def delete(request):
"""删除文章记录"""
# 接收id
id = request.POST.get('id')
back_dic = {'code': 200, 'msg': '删除成功'}
# 删除文章表,分类id也在文章表中
models.Article.objects.filter(pk=id).delete()
# 删除文章与标签的第三张表中的有关与这篇文章的数据
models.Article2Tag.objects.filter(article_id=id).delete()
return JsonResponse(back_dic)
富文本编辑器上传图片
图片上传不上去,需要自己更改接口,图片返回格式必须是json格式。
上传图片处理步骤:
1. 接收文件对象。不清除图片的key是什么,可以先打印request.FILES,或直接查看文档
2. 拼接图片上传文件夹的路径
3. 拼接图片的文件名
生成随机数
去除随机数中的'-'
拼接图片后缀
4. 拼接上传图片路径,文件夹路径,文件名
5. with open上传文件,二进制循环写入数据
6. 返回json格式数据
后端代码:
def upload(request):
"""上传图片,需要自己处理"""
"""
图片返回格式必须是json
//成功时
{
"error" : 0,
"url" : "http://www.example.com/path/to/file.ext" # 图片地址
}
//失败时
{
"error" : 1,
"message" : "错误信息"
}
"""
if request.method == 'POST':
# 1.接收文件对象
file_obj = request.FILES.get('imgFile')
print(request.FILES) # 不清除图片的key是什么,可以先打印
import os
from django.conf import settings
# 2.拼接图片上传文件夹的路径
article_img_path = os.path.join(settings.BASE_DIR, 'media', 'article_img')
if not os.path.exists(article_img_path): # 路径不存在创建文件夹
os.mkdir(article_img_path)
# 3. 拼接图片的文件名
# 小问题:直接使用文件名作为保存图片的名称,文件名重复时会覆盖情况,生成随机数
import uuid
s = str(uuid.uuid4()) # 形式是:dada-ddsa-dasdas-dasdas
new_s = s.replace('-', '') # 去除字符串中的'-'
file_name = new_s + '.' + file_obj.name.rsplit('.')[-1] # 新随机数.图片后缀
# 4. 拼接上传图片路径
path_file = os.path.join(article_img_path, file_name) # /media/article_img/123.png
# 5.上传文件,二进制写入数据
with open(path_file, 'wb') as f:
for line in file_obj:
f.write(line)
# 6.返回json格式数据
return JsonResponse({
"error": 0,
# "url": "/media/article_img/%s" % file_obj.name
"url": "%s" % path_file
})
使用别人的前端模板
混合项目,使用别人的页面样式:
使用别人的页面样式:
1.复制html粘贴到templates
2.复制css到static中
3.更改引入的路径,头跟尾部都查看一下
4.更改图片路径
5.然后自己加功能的路由,地址,视图函数
6.自己加js逻辑
ctrl+r 替换