Day 06 BBS项目05 评论功能+后台管理
一、根评论功能
前端
<div class="text-center" role="alert"></div> # 这里是一个警示框 发表评论成功与否 显示消息
{% if request.user.is_authenticated %}
<div class="media" style="margin-top: 15px;padding: 5px 5px 0 5px;"> # 这个是评论框
<img src="/media/{{ request.user.avatar }}" class="mr-3" alt="..."
style="width: 40px;height: 40px">
<div class="media-body row">
<div class="col-10">
<textarea class="form-control" id="exampleFormControlTextarea1" rows="3"
style="background-color: #ededed"></textarea>
</div>
<div class="col-2">
<button id="comment" class="btn btn-primary" style="width: 90%;height: 100%;">发表评论</button>
</div>
</div>
</div>
{% else %}
<p>你还没有登录,请去 <a href="/signin/">登录/</a><a href="/signup/">注册</a></p>
{% endif %}
<div id="new"> # 新的评论内容在这里展示
<p>
</div> # 这里展示所有评论
{% for commit in commits %}
<hr>
<div class="media-body row" style="font-size: 14px;color: #707070;padding: 2px">
<div class="col-1 user-face"><a href="/{{ commit.user.username }}">
<img src="/media/{{ commit.user.avatar }}" alt="" class="rounded-circle thumbnail"></a></div>
<div class="col">
<div class="user"><a
href="/{{ commit.user.username }}">{{ forloop.counter }}楼 {{ commit.user.username }}</a>
</div>
<p class="text" style="margin-top: 2px;color: black">{{ commit.content }}</p>
<div class="info"><span class="time">2020-10-31 11:33</span>
<span style="right: 100px;position: absolute" class="reply" pk="{{ commit.pk }}"
username="{{ commit.user.username }}">回复</span>
</div>
</div>
</div>
{% endfor %}
前端页面显示为这个
现在开始js代码
parent_id = '' # 先设置为空 在点击回复的时候 赋值
$('#comment').click(function () {
let content = $('div>textarea').val() # 获取评论的内容
$.ajax({
url: '/comment',
method: 'post',
data: {
content: content, # 评论内容
article_id:{{article.pk}}, # 文章id
parent: parent_id, # 父评论
csrfmiddlewaretoken: '{{ csrf_token }}'
},
success: (result) => {
# 给我们的警示框 添加样式 并赋予内容
$($('div[role="alert"]')).addClass('alert alert-danger').html(result.msg);
setTimeout(() => {
$($('div.alert')).removeClass('alert alert-danger').html(''),
$('div>textarea').val('')
}, 1200); # 1.2 秒后 警示框消失,并且 评论内容也清空
parent_id = '' # 评论完成后 父评论为空
let username = result.username
let avatar = result.avatar
if (result.code === 100) {
let str = ``; # 这里es6 字符串格式化 相当于 python中的 f{}asdas" 或者format
# ${xxx} 作为可变内容
str = `<hr>
<div class="media-body row" style="font-size: 14px;color: #707070;padding: 2px;box-shadow: 0 1px 1px 1px rgba(162, 162, 162, 0.1);">
<div class="col-1 user-face"><a href="/${username}">
<img src="/media/${avatar}" alt="" class="rounded-circle thumbnail"></a></div>
<div class="col">
<div class="user"><ahref="/${username}">${username}</a>
</div>
<p class="text" style="margin-top: 2px;color: black">${content}</p>
<div class="info"><span class="time">刚刚</span><span
class="like "></span><span style="right: 100px;position: absolute" id="reply"
class="${username}">回复</span>
</div>
</div>
</div>`
$('#new').append(str) # 评论成功 在评论列表下显示 自己的 最新评论
} else if (result.code === 102) {
str = `<hr>
<div class="media-body row" style="font-size: 14px;color: #707070;padding: 2px;box-shadow: 0 1px 1px 1px rgba(162, 162, 162, 0.1);">
<div class="col-1 user-face"><a href="/${username}">
<img src="/media/${avatar}" alt="" class="rounded-circle thumbnail"></a></div>
<div class="col">
<div class="user"><ahref="/${username}">${username}</a>
</div>
<p class="text" style="margin-top: 2px;color: black">@<a href="/${username}">${username}</a>${content}</p>
<div class="info"><span class="time">刚刚</span><span # 时间刚刚
class="like "></span><span style="right: 100px;position: absolute" id="reply"
class="${username}">回复</span>
</div>
</div>
</div>`
}
}
})
})
# 这里是 回复 也就是子评论内容
$('.reply').click(function () {
const parent_name = $(this).attr('username') # 拿到父评论者
parent_id = $(this).attr('pk') # 这里给我们的 父id 赋值 从此有了值 不再为空
$('div>textarea').val('@' + parent_name + '\n').focus() # 评论框 内容 +换行
})
后端
# urls.py
url('^comment$', views.comment),
# views.py
def comment(request):
result = {'code': 101, 'msg': '评论失败!'}
print(request.POST)
if request.is_ajax(): # 看看是不是ajax请求
content = request.POST.get('content')
article_pk = request.POST.get('article_id')
parent = request.POST.get('parent')
# 存入事件 评论保存 文章评论数+1
with transaction.atomic():
commit = models.Commit.objects.create(user=request.user, article_id=article_pk,
content=content)
models.Article.objects.filter(pk=article_pk).update(commit_num=F('commit_num') + 1)
# 如果有parent_id 实行与父评论关联
if parent:
commit.commit_id = models.Commit.objects.get(pk=parent)
commit.save()
result['code'] = 100
result['msg'] = '评论成功!'
result['username'] = commit.article.blog.user_info.username
result['content'] = content
# 这里只要是一个路径就可以了
result['avatar'] = str(request.user.avatar)
print(result)
return JsonResponse(result)
我们来看看效果
二、后台管理页面搭建
1.back_base.html+修改头像+修改密码(模态框)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="//at.alicdn.com/t/font_2145723_p6g61z1ymh8.css">
<!-- CSS only -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.0/dist/css/bootstrap.min.css"
integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
<!-- JS, Popper.js, and jQuery -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.0/dist/js/bootstrap.min.js"
integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI"
crossorigin="anonymous"></script>
<title>创作中心</title>
</head>
<body>
<style>
label {
margin-left: 10px;
}
html body {
background-color: #f4f4f4;
}
</style>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">首页</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav" style="width: 100%">
<li class="nav-item active" style="width: 20%">
<a class="nav-link" href="#"
style="background-color: #006edc;width:100px;height: 38px;border-radius: 10px">
<i class="iconfont icon-shipin"> I See</i>
<span class="sr-only">(current)</span></a>
</li>
<li class="nav-item" style="width:70%;">
<form class="form-inline my-2 my-lg-0">
<input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search"
style="width: 80%">
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">搜索</button>
</form>
</li>
<li class="nav-item dropdown" style="width: 10%">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button"
data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
54088
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="/{{ user.username }}">个人中心</a>
<a class="dropdown-item" href="/{{ user.username }}/create_article">创作中心</a>
<button type="button" class="dropdown-item" data-toggle="modal" data-target="#set_password_modal">
修改密码
</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item" id="sign-out">退出</button>
</div>
</li>
</ul>
</div>
</nav>
<div class="row">
<div class="col-3 text-center" style="margin-top: 30px;margin-left: 20px">
<div class="accordion" id="accordionExample">
<div class="card" style="border-radius: 5px">
<div class="card-header bg-primary">
<a href="/{{ user.username }}/create_article" class="btn btn-block text-center"><i
class="iconfont icon-chuanshu_shangchuan"></i>
投 稿</a>
</div>
</div>
<div class="card">
<div class="card-header" id="headingTwo">
<button class="btn btn-block text-center" type="button" data-toggle="collapse"
data-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
操 作
</button>
</div>
<div id="collapseTwo" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionExample">
<div class="card-body">
<p>
<button class="btn btn-block btn-primary text-center" data-toggle="modal"
data-target="#set_avatar_modal">修 改 头 像
</button>
</p>
<p>
<button class="btn btn-block btn-primary text-center" data-toggle="modal"
data-target="#set_password_modal">
修改密码
</button>
</p>
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="headingTwo">
<button class="btn btn-block text-center" type="button" data-toggle="collapse"
data-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
分 类
</button>
</div>
<div id="collapseThree" class="collapse" aria-labelledby="headingThree" data-parent="#accordionExample">
<div class="card-body">
<p class="bg-primary">
<a href="/{{ user.username }}/back/article" class="btn btn-block text-center">文 章</a>
</p>
<p class="bg-primary">
<a href="/{{ user.username }}/back/category" class="btn btn-block text-center">分 类</a>
</p>
<p class="bg-primary">
<a href="/{{ user.username }}/back/tag" class="btn btn-block text-center">标 签</a>
</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-8">
{% block content %}
{% endblock %}
</div>
</div>
<div class="modal fade" id="set_password_modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content col-10">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">修改密码</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="form-group col">
<form action="">{% csrf_token %}</form>
<i class="iconfont icon-suo"></i><label for="exampleInputPassword1">Password</label>
<input type="password" class="form-control" name="password">
<p class="text-right text-danger" style="font-size: 12px;height: 12px"></p>
</div>
<div class="form-group col">
<i class="iconfont icon-suo"></i><label for="exampleInputPassword1">RePassword</label>
<input type="password" class="form-control" name="re_password">
<p class="text-right text-danger" style="font-size: 12px;height: 12px"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="set_password">确认</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="set_avatar_modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content col-10">
<div class="modal-header">
<p><h5 class="modal-title" id="exampleModalLabel">修改头像</h5></p>
</div>
<div class="modal-body">
<label for="avatar" id="avatar_label"
style="
color: #006edc">选择头像
<img src="https://gitee.com/A1L19/i-see-project/raw/master/Avatar/%E5%A4%B4%E5%83%8F%20(1).jpg"
style="width: 80px;height:80px;border-radius: 50%;
background-size: 100%;background-repeat: no-repeat;
background-color: #f2f2f2;margin-left: 60px" id="avatar_img">
</label>
<input type="file" id="avatar" style="display: none">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="set_avatar">确认</button>
</div>
</div>
</div>
</div>
{% block script %}
{% endblock %}
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script>
$('#avatar').change(function () {
{#借助于文件阅读器#}
var filereader = new FileReader()
{#把图片读取到filereader中#}
{#读取文件#}
avtar = $('#avatar')[0].files[0]
filereader.readAsDataURL(avtar)
{#在读取文件时我们不能立马将文件写入到属性中,因为代码是立即执行的,写入时#}
{#可能还没有读取完成,所以我们要在读取完成后写入#}
filereader.onload = function () {
$('#avatar_img').attr('src', filereader.result)
}
})
{#修改密码操作#}
$('#set_password').click(function () {
$.ajax({
url: '/set_password/',
method: 'post',
data: {
'password': $('input[name="password"]').val(),
're_password': $('input[name="re_password"]').val(),
'csrfmiddlewaretoken': $('[name="csrfmiddlewaretoken"]').val()
},
success: (result) => {
alert(result.msg)
location.href = result.path
}
})
})
$('#set_avatar').click(function () {
let formData = new FormData()
formData.append('avatar', $('#avatar')[0].files[0])
console.log($('#avatar')[0].files[0])
$.ajax({
url: '/set_avatar/',
type:'post',
processData: false,
contentType: false,
data: formData,
success: (result) => {
alert(result.msg)
}
})
})
</script>
</body>
</html>
1.1修改头像+修改密码后端
# urls.py
url(r'^set_password/', views.set_password),
url(r'^set_avatar/', views.set_avatar),
# views.py
# 修改密码
@login_required(login_url='/signin/')
def set_password(request):
result = {'code': 101, 'msg': '密码输入错误!', 'path': '/home/'}
if request.method == 'POST':
print(request.POST)
password = request.POST.get('password')
re_password = request.POST.get('re_password')
if not request.user.is_authenticated():
return JsonResponse(result)
else:
if request.user.check_password(password):
request.user.set_password(re_password)
request.user.save()
result['code'] = 100
result['msg'] = '密码修改成功!'
result['path'] = '/signin/'
return JsonResponse(result)
# 修改头像
from django.views.decorators.csrf import csrf_exempt
@login_required(login_url='/signin/')
@csrf_exempt
def set_avatar(request):
user = request.user
result = {'msg': '修改成功'}
print(request.FILES.get('avatar'))
if request.method == 'POST':
new_avatar = request.FILES.get('avatar')
user.avatar = new_avatar
user.save()
return JsonResponse(result)
2.后端+内容显示
前端
{% extends 'background/backstage_base.html' %}
{% block content %}
<div class="col-6" style="margin-left: 50px">
<div>
{% if code == 101 %}
<h3 style="margin-left: 80px">{{ articles }}</h3>
{% else %}
{% for article in articles %}
<div class="media" style="margin-top: 15px">
<img src="/media/{{ article.blog.user_info.avatar }}" class="mr-3" alt="..."
style="width: 40px;height: 40px">
<div class="media-body">
<h5 class="mt-0"><a
href="/{{ article.blog.user_info.username }}/article/{{ article.id }}.html">{{ article.title }}</a>
</h5>
{{ article.introduction }}
<div>
<span style="font-size: 12px;height: 12px"><a
href="/{{ article.blog.user_info.username }}">{{ article.blog.user_info.username }} </a>
{{ article.publish_time|date:'Y-m-d H-i-s' }} </span>
</div>
</div>
</div>
{% endfor %}
{% endif %}
</div>
</div>
{% endblock %}
后端
# urls.py
url('^(?P<name>\w+)/back/(?P<type>category|tag|article)$', views.user_background),
# views.py
def user_background(request, name, **kwargs):
user = models.User_info.objects.filter(username=name).first()
print(kwargs)
if user:
# 默认显示作者全部文章
articles = models.Article.objects.filter(blog=user.blog)
query = kwargs.get('type', None)
if user.blog:
# 根据类别筛选出显示的文章
if query == 'category':
articles = articles.order_by('category')
elif query == 'tag':
articles = articles.order_by('tag')
return render(request, 'background/user_background.html',
{'articles': articles, 'user': user})
return render(request, 'background/user_background.html',
{'articles': '这个作者很懒什么都没留下!', 'user': user, 'code': 101})
三、富文本编辑器的使用
# 1 下载kindeditor:http://kindeditor.net/down.php
# 2 解压缩,把整个文件加放入static目录下
# 3 再add_article.html中
<div class="form-group">
<label for="">内容</label>
<textarea name="content" id="editor_id" cols="80" rows="10" class="form-control"> </textarea>
</div>
# 3 js代码
<script charset="utf-8" src="/static/kindeditor/kindeditor-all.js"></script>
<script>
KindEditor.ready(function (K) {
window.editor = K.create('#editor_id', {
width: '100%',
height: '500px',
resizeType: 1,
});
});
四、新增文章,处理xss攻击
# pip3 install beautifulsoup4
from bs4 import BeautifulSoup
@login_required(login_url='/signin/')
def create_article(request, name):
print(request.POST)
categorys = models.Category.objects.order_by('name')
tags = models.Tag.objects.order_by('name')
if request.method == 'POST':
title = request.POST.get('title')
content = request.POST.get('content')
category = request.POST.get('category')
tag = request.POST.getlist('tag')
soup = BeautifulSoup(content, 'html.parser')
introduction = soup.text[0:90] # 去掉所有标签后的文本
# 把script标签干掉
res_script = soup.find_all('script')
for script in res_script:
script.decompose() # 删除script标签
article = models.Article.objects.create(title=title, content=str(soup), introduction=introduction,
blog=request.user.blog,
category_id=category)
article.tag.add(*tag)
return redirect('/user_background/')
return render(request, 'background/create_article.html', {'categorys': categorys, 'tags': tags})
五、富文本编辑器上传图片
前端代码
<script charset="utf-8" src="/static/kindeditor/kindeditor-all.js"></script>
<script>
KindEditor.ready(function (K) {
window.editor = K.create('#content', {
width: '100%',
height: '500px',
resizeType: 1,
uploadJson: '/upload_img/',
filePostName: 'myfile',
//额外带的参数
extraFileUploadParams: {
csrfmiddlewaretoken: '{{ csrf_token }}',
}
});
});
</script>
后端
def upload_img(request):
result = {'error': 0}
try:
file = request.FILES.get('myfile')
path = os.path.join(settings.BASE_DIR, 'media', 'image', file.name)
with open(path, 'wb')as f:
for line in file:
f.write(line)
result['url'] = '/media/image/' + file.name
except Exception:
result['error'] = 1
result['message'] = '文件上传失败!'
return JsonResponse(result)