1211 BBS后台管理文章添加
目录
昨日内容回顾
侧边栏inclusion_tag
inclusion_tag的响应
- 可以将页面上的某个区域的内容坐火车哪个模块的形式
- 调用的时候传入参数,即可渲染出对应的页面放到调用的位置
使用
- 当页面上的某一块内容在多个页面上都需要别使用到
- 并且这块内容是需要传参才能渲染出来,那么你可以考虑使用
- inclusion_tag来帮你简化代码
自定义inclusion_tag,标签,过滤器
-
在应用下新建一个名字必须叫template_tag文件夹
-
在该文件夹内新建任意名称的py文件
-
在py文件内,固定先写下面的两句代码
文章的点赞点踩
前端
- 点赞点踩前端样式拷贝
除了拷贝html代码之外
还需要将配套的css代码也拷贝过来
如果css代码中引用了图片 为了防止图片防盗链 应该下载到本地
-
点击提交点赞点踩
如何区分用户点赞还是点踩 给点赞点踩标签统一加了一个相同的类名 给该类名绑定点击事件 利用this指点的是当前被操作对象本身再加上判断某个标签是否有某个属性 能够得出用户点的赞还是踩(hasClass())
-
向后端发送ajax请求
- 点赞点踩
- csrf
- 文章id
-
回调函数
展示提示信息 成功 提示 将对于的数字加一 一定要转数值类型再相加 失败 提示
后端
校验规则
-
校验当前请求是ajax请求
`request.is_ajax()`
-
校验当前用户是否登录
`request.user.is_authenticated()`
-
校验的当前用户是否点的是自己的文章
根据文章id获取文章对象,利用文在哪个对象查询改文章的作者 与当前登录的用户进行比对 request.user == article_obj.blog.userinfo
-
校验当前用户是否已经给当前文章点过了
只需要去点赞点踩表中查询用户是当前用户并且文章id是前端传来的id
-
操作数据库
两个地方需要修改 点赞点踩表 文章表中的普通字段
文章的评论功能
先考虑根评论 再考虑子评论
1.前端
渲染评论框
区分当前用户是否登录
没有登录的情况下,只展示登录注册
鞥路的情况下才展示评论框
-
给提交的评论按钮绑定点击事件
发送ajax请求
评论内容 文章id csrf
回调函数
将评论框中内容清空
临时渲染
利用模板字符串 利用DOM操作将临时创建的模板字符串添加到正常的文档流中
刷新渲染
手动添加html代码
-
子评论
研究点击回复按钮发生的事情
1.将当前想要评论的评论人的姓名 2.将拼接号的内容添加到评论框中 3.评论框自动聚焦 focus()
提交根评论与提交子评论点击的是相同的按钮
根评论与子评论唯一的区别仅仅在于是否有parentid赋值
朝后端发送
后端parentid无论是否传值都不会影响存储 所以你的提交代码写一份就可以
需要避免的问题
提交子评论之后一定要将全局变量名充值为空
在存储数据的时候,要对数据进行切割操作
在渲染评论楼的时候,针对子评论需要多渲染一个根评论人的名字
comment.parent.user.username
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">
Collapsible Group Item #1
</a>
</h4>
</div>
<div id="collapseOne" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingOne">
<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>
<!-- 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">Home</a></li>
<li role="presentation"><a href="#profile" aria-controls="profile" role="tab" data-toggle="tab">Profile</a></li>
<li role="presentation"><a href="#messages" aria-controls="messages" role="tab" data-toggle="tab">Messages</a></li>
<li role="presentation"><a href="#settings" aria-controls="settings" role="tab" data-toggle="tab">Settings</a></li>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="home">...</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>
</div>
在右侧的侧边栏设置模板,用来继承整体显示
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="home">
{% block article %}
文章页面
{% endblock %}
</div>
<div role="tabpanel" class="tab-pane" id="profile">
随笔页面
</div>
<div role="tabpanel" class="tab-pane" id="messages">3</div>
<div role="tabpanel" class="tab-pane" id="settings">4</div>
</div>
文件模板继承
{% extends 'backend/backend_base.html' %}
{#文章页面展示#}
{% block article %}
<table class="table table-striped table-hover">
<thead>
<tr>
<th>标题</th>
<th>发布时间</th>
<th>评论数</th>
<th>点赞数</th>
<th>操作</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for article in article_list %}
<tr>
<td><a href="/{{ request.user.user.username }}/article/{{ article.pk }}">{{ article.title }}</a></td>
<td>{{ article.create_time|date:'Y-m-d' }}</td>
<td>{{ article.comment_num }}</td>
<td>{{ article.up_num }}</td>
<td><a href="#">编辑</a></td>
<td><a href="#">删除</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
使用分页器
class Pagination(object):
def __init__(self,current_page,all_count,per_page_num=2,pager_count=11):
"""
封装分页相关数据
:param current_page: 当前页
:param all_count: 数据库中的数据总条数
:param per_page_num: 每页显示的数据条数
:param pager_count: 最多显示的页码个数
用法:
queryset = model.objects.all()
page_obj = Pagination(current_page,all_count)
page_data = queryset[page_obj.start:page_obj.end]
获取数据用page_data而不再使用原始的queryset
获取前端分页样式用page_obj.page_html
"""
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)
文章的发布
页面的搭建
编辑器的选用kindeditor编辑器
kindeditor编辑器
使用
下载
部署
修改
html添加以下代码
<script charset="utf-8" src="/editor/lang/zh-CN.js"></script>
<script>
KindEditor.ready(function(K) {
window.editor = K.create('#editor_id');
});
</script>
初始参数
输入框的长宽设置
<script>
KindEditor.ready(function (K) {
window.editor = K.create('#id_comment',{
width: '100%',
height:'450px',
resizeType:0
});
});
</script>
items
配置编辑器的工具栏,其中”/”表示换行,”|”表示分隔符。
- 数据类型: Array
- 默认值:
[
'source', '|', 'undo', 'redo', '|', 'preview', 'print', 'template', 'code', 'cut', 'copy', 'paste',
'plainpaste', 'wordpaste', '|', 'justifyleft', 'justifycenter', 'justifyright',
'justifyfull', 'insertorderedlist', 'insertunorderedlist', 'indent', 'outdent', 'subscript',
'superscript', 'clearhtml', 'quickformat', 'selectall', '|', 'fullscreen', '/',
'formatblock', 'fontname', 'fontsize', '|', 'forecolor', 'hilitecolor', 'bold',
'italic', 'underline', 'strikethrough', 'lineheight', 'removeformat', '|', 'image', 'multiimage',
'flash', 'media', 'insertfile', 'table', 'hr', 'emoticons', 'baidumap', 'pagebreak',
'anchor', 'link', 'unlink', '|', 'about'
]
source | HTML代码 |
---|---|
preview | 预览 |
undo | 后退 |
redo | 前进 |
cut | 剪切 |
copy | 复制 |
paste | 粘贴 |
plainpaste | 粘贴为无格式文本 |
wordpaste | 从Word粘贴 |
selectall | 全选 |
justifyleft | 左对齐 |
justifycenter | 居中 |
justifyright | 右对齐 |
justifyfull | 两端对齐 |
insertorderedlist | 编号 |
insertunorderedlist | 项目符号 |
indent | 增加缩进 |
outdent | 减少缩进 |
subscript | 下标 |
superscript | 上标 |
formatblock | 段落 |
fontname | 字体 |
fontsize | 文字大小 |
forecolor | 文字颜色 |
hilitecolor | 文字背景 |
bold | 粗体 |
italic | 斜体 |
underline | 下划线 |
strikethrough | 删除线 |
removeformat | 删除格式 |
image | 图片 |
flash | Flash |
media | 视音频 |
table | 表格 |
hr | 插入横线 |
emoticons | 插入表情 |
link | 超级链接 |
unlink | 取消超级链接 |
fullscreen | 全屏显示 |
about | 关于 |
打印 | |
code | 插入程序代码 |
map | Google地图 |
baidumap | 百度地图 |
lineheight | 行距 |
clearhtml | 清理HTML代码 |
pagebreak | 插入分页符 |
quickformat | 一键排版 |
insertfile | 插入文件 |
template | 插入模板 |
anchor | 插入锚点 |
上传文章里图片文件
开设url地址设置
uploadJson:'地址'
extraFileUploadParams 参数设置
csrf验证
上传图片、Flash、视音频、文件时,支持添加别的参数一并传到服务器。
- 数据类型: Array
- 默认值: {}
KindEditor.ready(function(K) {
K.create('#id', {
extraFileUploadParams : {
item_id : 1000,
category_id : 1
}
});
});
post提交数据
返回格式(json)
成功时
{
"error":0,
"url":文件路径
}
失败时
{
"error":1,
"message":"错误信息"
}
防止XSS攻击
文章的简介如何获取
- 直接将用户输入的原生的script标签直接删除
- 将用户输入的原生script标签直接转义(
包括
)
如何从html代码中筛选出纯文本内容
如何从html代码中删除script标签
使用beautifulsoup
进行筛选
from bs4 import BeautifulSoup
@login_required
def add_article(request):
tag_list = models.Tag.objects.filter(blog=request.user.blog)
category_list = models.Category.objects.filter(blog=request.user.blog)
# 判断用户输入的文章,并获取
if request.method == 'POST':
title = request.POST.get('title')
content = request.POST.get('content')
tags_list = request.POST.getlist('tags')
category_id = request.POST.get('category')
# 先生成一个模块对象
soup = BeautifulSoup(content,'html.parser')
# soup.text # 获取纯文本
tags = soup.find_all() # 获取所有的标签文本
for tag in tags:
if tag.name == 'script':
tag.decompose() # 删除script标签
# 文章简介,截取文章的前150字符
desc = soup.text[0:150]
# 操作数据库
article_obj = models.Article.objects.create(title=title,content=str(soup),desc=desc,category_id=category_id,blog=request.user.blog)
# 去文章与标签的第三张表中手动录入数据,利用bulk_create
obj_list = []
for tag_id in tags_list:
obj_list.append(models.Article2Tag(article=article_obj,tag_id=tag_id))
models.Article2Tag.objects.bulk_create(obj_list)
# 重定向
return redirect('/backend/')
return render(request,'backend/add_article.html',locals())
代码
后台管理
{% extends 'backend/backend_base.html' %}
{#文章页面展示#}
{% block article %}
<table class="table table-striped table-hover">
<thead>
<tr>
<th>标题</th>
<th>发布时间</th>
<th>评论数</th>
<th>点赞数</th>
<th>操作</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for article in page_queryset %}
<tr>
<td><a href="/{{ request.user.username }}/article/{{ article.pk }}">{{ article.title }}</a></td>
<td>{{ article.create_time|date:'Y-m-d' }}</td>
<td>{{ article.comment_num }}</td>
<td>{{ article.up_num }}</td>
<td><a href="#">编辑</a></td>
<td><a href="#">删除</a></td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="text-right">{{ page_obj.page_html|safe }}</div>
{% endblock %}
添加文章
add_article.py
{% extends 'backend/backend_base.html' %}
{% block article %}
<h3>添加文章</h3>
<form action="" method="post">
{% csrf_token %}
<p>标题</p>
<p><input type="text" name="title" class="form-control"></p>
<p>内容使用kind编辑器</p>
<p>
<textarea name="content" id="id_comment" cols="30" rows="10" class="form-control"></textarea>
</p>
<p>标签</p>
<p>
{# 渲染标签#}
{% for tag in tag_list %}
<input type="checkbox" name="tags" value="tag.pk"> {{ tag.name }}
{% endfor %}
</p>
<p>分类</p>
<p>
{# 渲染分类#}
{% for category in category_list %}
<input type="radio" name="category" value="category.pk"> {{ category.name }}
{% endfor %}
</p>
<input type="submit" class="btn btn-success">
</form>
{#kindeditor编辑器的部署#}
<script charset="utf-8" src="/static/kindeditor/kindeditor-all-min.js"></script>
<script>
KindEditor.ready(function (K) {
window.editor = K.create('#id_comment',{
width: '100%',
height:'450px',
resizeType:0,
uploadJson:'/upload_img/',
extraFileUploadParams : {
csrfmiddlewaretoken:'{{ csrf_token }}'
}
});
});
</script>
{% endblock %}
更改头像
set_avatar
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户更改头像</title>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
{% load static %}
<link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}">
<script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}"></script>
</head>
<body>
<form action="" method="post" enctype="multipart/form-data">
{% csrf_token %}
<h3>原头像:</h3>
<img src="/media/{{ request.user.avatar }}" alt="">
<div class="form-group">
<label for="mdd">新头像
<img src="/static/img/default.jpg" alt="" width="100px" style="margin-left: 10px" id="img">
</label>
{# 将input上传文件的框隐藏#}
<input type="file" name="avatar" id="mdd" style="display: none;">
</div>
<input type="submit" class="btn btn-primary">
</form>
<script>
$('#mdd').on('change',function () {
{#内置对象FileReader,完成文件的读取操作#}
let MyFileReader = new FileReader();
// 获取用户上传的文件对象
let fileObj = $(this)[0].files[0];
// 让文件阅读器读取文件
MyFileReader.readAsDataURL(fileObj); // 异步io操作
// 等待文件io操作读取完成再执行下面的代码
MyFileReader.onload = function () {
// 将读取之后的内容替换到img标签src属性中
$('#img').attr('src',MyFileReader.result)
}
});
</script>
</body>
</html>
后台管理
from app01.utils.mypage import Pagination
# 后台管理
@login_required
def backend(request):
article_list = models.Article.objects.filter(blog=request.user.blog)
# 分页器的使用
page_obj = Pagination(current_page=request.GET.get('page',1),all_count=article_list.count())
page_queryset = article_list[page_obj.start:page_obj.end]
return render(request,'backend/backend.html',locals())
from bs4 import BeautifulSoup
@login_required
def add_article(request):
tag_list = models.Tag.objects.filter(blog=request.user.blog)
category_list = models.Category.objects.filter(blog=request.user.blog)
# 判断用户输入的文章,并获取
if request.method == 'POST':
title = request.POST.get('title')
content = request.POST.get('content')
tags_list = request.POST.getlist('tags')
category_id = request.POST.get('category')
# 先生成一个模块对象
soup = BeautifulSoup(content,'html.parser')
# soup.text # 获取纯文本
tags = soup.find_all() # 获取所有的标签文本
for tag in tags:
if tag.name == 'script':
tag.decompose() # 删除script标签
# 文章简介,截取文章的前150字符
desc = soup.text[0:150]
# 操作数据库
article_obj = models.Article.objects.create(title=title,content=str(soup),desc=desc,category_id=category_id,blog=request.user.blog)
# 去文章与标签的第三张表中手动录入数据,利用bulk_create
obj_list = []
for tag_id in tags_list:
obj_list.append(models.Article2Tag(article=article_obj,tag_id=tag_id))
models.Article2Tag.objects.bulk_create(obj_list)
# 重定向
return redirect('/backend/')
return render(request,'backend/add_article.html',locals())
import os
from BBS import settings
# 文章内上传文件的函数
def upload_img(request):
back_dic = {'error':0}
if request.method == 'POST':
# print(request.FILES) # 打印键值对
# 获取用户上传的图片,然后保存在本地
file_obj = request.FILES.get('imgFile')
# 手动拼接存储的文件路径
file_path = os.path.join(settings.BASE_DIR,'media','article_img')
if not os.path.isdir(file_path):
os.mkdir(file_path)
# 保存文件
img_path = os.path.join(file_path,file_obj.name)
with open(img_path,'wb') as f:
for l in file_obj:
f.write(l)
# 成功返回0,back_dic
_url = '/media/article_img/%s'%file_obj.name
back_dic['url'] = _url
return JsonResponse(back_dic)
@login_required
def set_avatar(request):
# 展示用户之前的头像,用户上传新的头像
if request.method == 'POST':
file_obj = request.FILES.get('avatar')
# 方式一,不会自动拼接路径
models.Userinfo.objects.filter(pk=request.user.pk).update(avatar=file_obj)
# 方式二
request.user.avatar = file_obj
request.user.save()
return redirect('/home/')
return render(request,'set_avatar.html',locals())