模型层其他知识点(FileField/数据库字段设计优化/自关联)
from django.db import models
from django.contrib.auth.models import AbstractUser
class UserInfo(AbstractUser):
# 头像
avatar = models.FileField(
verbose_name="用户头像",
upload_to="avatar/", # 先创建一个avatar文件夹
default="avatar/default.png", # 在avatar文件夹下放一张默认的default.png
)
"""
给avatar字段传文件对象,该文件会自动存储到avatar文件下,然后avatar字段只保存文件路径avatar/default.png
"""
class Article(models.Model):
# 数据库字段设计优化,这三个字段在多对多的第三张表中也有,在这里也设置,因为到时候查询时不需要频繁的进行跨表查询,效率能够提高;在插入数据时,多对多第三张表插入的同时,在这张表也同时插入数据
up_num = models.BigIntegerField(
verbose_name="点赞数",
default=0,
)
down_num = models.BigIntegerField(
verbose_name="点踩数",
default=0,
)
comment_num = models.BigIntegerField(
verbose_name="评论数",
default=0,
)
class Comment(models.Model):
# 自关联,记得null=True参数必须要有
parent = models.ForeignKey(
to="self",
null=True,
on_delete=models.CASCADE,
)
存储用户文件的时候,文件名加一个前缀,避免文件名冲突
"""
一般情况下我们在存储用户文件的时候,为了避免文件名冲突的情况,会给文件名加一个前缀
比如:uuid/随机字符串...
这里我用固定的'asd'代替了,可以把其替换为uuid或随机字符串
"""
file_obj = request.FILES.get("avatar", "default.png")
file_obj.name = "asd" + file_obj.name
模板语法form.auto_id
{% for form in form_obj %}
<div class="form-group">
<label for="{{ form.auto_id }}">{{ form.label }}</label>
{{ form }}
<span style="color: red" class="pull-right">{{ form.errors.0 }}</span>
</div>
{% endfor %}
<!--form.auto_id代指forms组件渲染出的标签的id值-->
前端知识点(文件阅读器对象)
<!--用户上传图片后,能够实时在页面展示该图片-->
<script>
$("#myfile").change(function () {
// 文件阅读器对象
// 1 先生成一个文件阅读器对象
let myFileReaderObj = new FileReader();
// 2 获取用户上传的头像文件
let fileObj = $(this)[0].files[0];
// 3 将文件对象交给阅读器对象读取
myFileReaderObj.readAsDataURL(fileObj); // 异步操作 IO操作;没有读取完毕就加载了设置src属性那一步操作,所以加一个onload
// 4 利用文件阅读器将文件展示到前端页面 修改src属性
// 等待文件阅读器加载完毕之后再执行
myFileReaderObj.onload = function () {
$("#myimg").attr("src", myFileReaderObj.result);
};
});
<script>
forms组件渲染出的标签,发送Ajax请求
<form id="myform"> <!--不用form表单提交,只是用一下form标签-->
{% csrf_token %}
{% for form in form_obj %}
<div class="form-group">
<label for="{{ form.auto_id }}">{{ form.label }}</label>
{{ form }}
<span style="color: red" class="pull-right">{{ form.errors.0 }}</span>
</div>
{% endfor %}
<div class="form-group">
<label for="myfile">
头像
<img src="{% static 'img/default.png' %}" alt="" width="100" style="margin-left: 10px" id="myimg">
</label>
<input type="file" id="myfile" name="avatar" style="display: none">
</div>
<input type="button" value="提交注册" class="btn btn-block btn-primary" id="id_commit">
</form>
<script>
$("#id_commit").click(function () {
// 发送Ajax请求,我们发送的数据中即包含键值对也包含文件
let formDateObj = new FormData();
{#console.log($("#myform").serializeArray());#}
// [{name: 'csrfmiddlewaretoken', value: 'qnkJIpAR39gFBz8yktflIru6V7678wYeiDwQvmWPa1DHzUnWEUC200VNwwurz6e9'},
// {name: 'username', value: 'admin'},
// {name: 'password', value: '123'},
// {name: 'confirm_password', value: '123'},
// {name: 'email', value: '2848890508@qq.com'}]
// 1 添加普通键值对
$.each($("#myform").serializeArray(), function (index, obj) {
formDateObj.append(obj.name, obj.value);
});
// 2 添加文件数据
formDateObj.append("avatar", $("#myfile")[0].files[0]);
// 3 发送Ajax请求
$.ajax({
url: "",
type: "post",
data: formDateObj,
contentType: false,
processData: false,
success: function (data) {
if (data.code===1000) {
location.href = data.url;
}else {
// console.log(data.msg); // 数据格式:{username: ["账号不能为空"], password: ["密码不能为空"]}
// 如何将对应的错误提示展示到对应的input框下面
// form组件渲染的标签的id值都是id_字段名
$.each(data.msg, function (index, obj) {
// index:username
// obj:["账号不能为空"]
let targetId = "#id_" + index;
$(targetId).next().text(obj[0]).parent().addClass("has-error")
})
}
},
});
});
$("input").focus(function () {
$(this).next().text('').parent().removeClass("has-error");
});
</script>
图片验证码
# 图片相关模块pillow的导入是PIL
from PIL import Image, ImageDraw, ImageFont
import random
# 内存管理器模块io
from io import BytesIO, StringIO
import string
def get_random():
return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)
def get_code(request):
# 推导步骤1:直接获取后端现成的二进制数据发给前端
# with open(r'static/img/default.png', 'rb') as f:
# data = f.read()
# return HttpResponse(data)
# 推导步骤2:利用pillow模块动态产生图片
# # img_obj = Image.new("RGB", (260, 35), "red") # 第一个参数"RGB"可写死,第二个参数代表长宽,第三个参数代表颜色
# img_obj = Image.new("RGB", (260, 35), get_random()) # 除了放'red'参数,还可以放三基色,元组放三个参数,每个参数0-255
# # 先将图片对象保存起来,再将图片对象读取出来
# with open("xxx.png", "wb") as f:
# img_obj.save(f, "png") # 保存图片,第一个参数是文件句柄,第二个参数代表图片格式
# with open("xxx.png", "rb") as f:
# data = f.read()
# return HttpResponse(data)
# 推导步骤3:文件存储繁琐IO操作效率低,借助于内存管理器模块
# img_obj = Image.new("RGB", (260, 35), get_random())
# io_obj = BytesIO() # 生成一个内存管理器对象,可以看成是文件句柄
# img_obj.save(io_obj, "png")
# return HttpResponse(io_obj.getvalue()) # 从内存管理器中读取二进制的图片数据返回给前端
# 最终步骤4:写图片验证码
img_obj = Image.new("RGB", (260, 35), get_random())
img_draw = ImageDraw.Draw(img_obj) # 产生一个画笔对象
img_font = ImageFont.truetype("static/font/222.ttf", 30) # 第一个参数是字体样式 第二个参数是字体大小
# 字体样式下载网站:http://www.zhaozi.cn/s/all/ttf/
"""我们计算机之所以能够输出各式各样的字体样式,内部其实对应的是一个个.ttf结尾的文件"""
# 五位数的随机验证码,数字-小写字母-大写字母
code = ''
for i in range(5):
random_int = random.choice(string.digits)
random_upper = random.choice(string.ascii_uppercase)
random_lower = random.choice(string.ascii_lowercase)
tmp = random.choice([random_int, random_lower, random_upper])
# 将产生的随机字符串写入到图片上
"""为什么一个个写而不是生成好了之后再写?因为一个个写能够控制每个字的间隙,而生成好之后再写,间隙就没法控制了"""
img_draw.text((i*45+10, -2), tmp, get_random(), img_font) # 第一个参数是横纵坐标,第二个参数是写的字符串,第三个参数是三基色,第四个参数是字体对象
# 拼接随机字符串
code += tmp
# 随机验证码在登录的视图函数里面需要用到,要比对,所以要找地方存起来,并且其他视图函数也能拿到,存到session
request.session["code"] = code
io_obj = BytesIO()
img_obj.save(io_obj, "png")
return HttpResponse(io_obj.getvalue())
<!--img标签的src属性:1.图片路径 2.url 3.图片的二进制数据-->
<img src="/get_code/" alt="" width="260" height="35" id="id_img">
<script>
// 给验证码图片标签添加一个点击事件,每一次点击都在/get_code/后面加一个问号'?',这样就能点一次就请求一次url做到刷新验证码的作用
$("#id_img").click(function () {
// 先获取标签之前的src
let oldVal = $(this).attr("src");
$(this).attr("src", oldVal += "?");
});
</script>
admin后台管理
"""
django给你提供了一个可视化的界面用来让你方便的对你的模型表
进行数据的增删改查操作
如果你先想要使用amdin后台管理操作模型表
你需要先注册你的模型表告诉admin你需要操作哪些表
去你的应用下的admin.py中注册你的模型表
from django.contrib import admin
from app01 import models
# Register your models here.
admin.site.register(models.UserInfo)
admin.site.register(models.Blog)
admin.site.register(models.Category)
admin.site.register(models.Tag)
admin.site.register(models.Article)
admin.site.register(models.Article2Tag)
admin.site.register(models.UpAndDown)
admin.site.register(models.Comment)
"""
# admin会给每一个注册了的模型表自动生成增删改查四条url
http://127.0.0.1:8000/admin/app01/userinfo/ 查
http://127.0.0.1:8000/admin/app01/userinfo/add/ 增
http://127.0.0.1:8000/admin/app01/userinfo/1/change/ 改
http://127.0.0.1:8000/admin/app01/userinfo/1/delete/ 删
http://127.0.0.1:8000/admin/app01/blog/ 查
http://127.0.0.1:8000/admin/app01/blog/add/ 增
http://127.0.0.1:8000/admin/app01/blog/1/change/ 改
http://127.0.0.1:8000/admin/app01/blog/1/delete/ 删
"""
关键点就在于urls.py中的第一条自带的url
前期我们需要自己手动苦逼的录入数据,自己克服一下
"""
# 1.数据绑定尤其需要注意的是用户和个人站点不要忘记绑定了
# 2.标签
# 3.标签和文章
千万不要把别人的文章绑定标签
# 模型表中的元类设置,能够修改admin后台管理显示数据表为中文名
class Userinfo(models.Model):
pass
class Meta:
verbose_name_plural = "用户表"
文章用户头像展示
"""
1 网址所使用的静态文件默认放在static文件夹下
2 用户上传的静态文件也应该单独放在某个文件夹下
media配置
该配置可以让用户上传的所有文件都固定存放在某一个指定的文件夹下
# 配置用户上传的文件存储位置
MEDIA_ROOT = os.path.join(BASE_DIR,'media') # 文件名随你自己
会自动创建多级目录(media/avatar---用户头像之前设置的是存放在avatar文件夹下的)
如何开设后端指定文件夹资源
首先你需要自己去urls.py书写固定的代码
from django.views.static import serve
from BBS14 import settings
# 暴露后端指定文件夹资源
url(r'^media/(?P<path>.*)$', serve, {'document_root':settings.MEDIA_ROOT})
re_path(r'^media/(?P<path>.*)$', serve, {'document_root':settings.MEDIA_ROOT})
"""
图片防盗链
# 如何避免别的网站直接通过本网站的url访问本网站资源
# 简单的防盗
我可以做到请求来的时候先看看当前请求是从哪个网站过来的
如果是本网站那么正常访问
如果是其他网站直接拒绝
请求头里面有一个专门记录请求来自于哪个网址的参数
Referer: http://127.0.0.1:8000/xxx/
# 如何避免
1.要么修改请求头referer
2.直接写爬虫把对方网址的所有资源直接下载到我们自己的服务器上
个人站点样式页面设计,及侧边栏筛选功能
# urls.py
# 个人站点页面搭建
path("<str:username>/", views.site, name="app01_site"),
# 文章分类、文章标签、文章日期归档(侧边栏筛选)
path("<str:username>/<str:condition>/<str:param>/", views.site),
# views.py
def site(request, username, **kwargs):
# 先校验当前用户名对应的个人站点是否存在
user_obj = UserInfo.objects.filter(username=username).first()
# 用户如果不存在,应该返回一个404页面
if not user_obj:
return render(request, "errors.html")
blog = user_obj.blog
# 查询当前个人站点下的所有的文章
article_list = Article.objects.filter(blog=blog)
# 查询当前用户所有的分类及分类下的文章数
category_list = Category.objects.filter(blog=blog).annotate(count_num=Count("article__pk")).values_list("name", "count_num", "pk")
# 查询当前用户所有的标签及标签下的文章数
tag_list = Tag.objects.filter(blog=blog).annotate(count_num=Count("article__pk")).values_list("name", "count_num", "pk")
# 按照年月统计所有的文章(ORM提供的查询,此时settings.py中的TIME_ZONE和USE_TZ需要进行配置)
date_list = Article.objects.filter(blog=blog).annotate(month=TruncMonth("create_time")).values("month").annotate(count_num=Count("pk")).values_list("month", "count_num")
# 侧边栏筛选
if kwargs:
condition = kwargs.get("condition")
param = kwargs.get("param")
# 判断用户到底想按照哪个条件筛选数据
if condition == "category":
article_list = article_list.filter(category_id=param)
elif condition == "tag":
article_list = article_list.filter(tags__id=param)
else:
year, month = param.split("-")
article_list = article_list.filter(create_time__year=year, create_time__month=month)
return render(request, "site.html", locals())
# site.html(文章分类、文章标签、文章日期归档---侧边栏筛选)
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">文章分类</h3>
</div>
<div class="panel-body">
{% for category in category_list %}
<p>
<a href="/{{ username }}/category/{{ category.2 }}/">{{ category.0 }}({{ category.1 }})</a>
</p>
{% endfor %}
</div>
</div>
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title">文章标签</h3>
</div>
<div class="panel-body">
{% for tag in tag_list %}
<p>
<a href="/{{ username }}/tag/{{ tag.2 }}/">{{ tag.0 }}({{ tag.1 }})</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 date in date_list %}
<p><a href="/{{ username }}/archive/{{ date.0|date:'Y-m' }}/">{{ date.0|date:'Y-m' }}({{ date.1 }})</a></p>
{% endfor %}
</div>
</div>
# 日期查询
"""
django官网提供的一个orm语法
from django.db.models.functions import TruncMonth
-官方提供
from django.db.models.functions import TruncMonth
Sales.objects
.annotate(month=TruncMonth('timestamp')) # Truncate to month and add to select list
.values('month') # Group By month
.annotate(c=Count('id')) # Select the count of the grouping
.values('month', 'c') # (might be redundant, haven't tested) select month and count
时区问题报错
TIME_ZONE = 'Asia/Shanghai'
USE_TZ = False
"""
文章详情页
# url设计
/username/article/1
# 先验证url是否会被其他url顶替
# 文章详情页和个人站点基本一致 所以用模版继承
# 侧边栏的渲染需要传输数据才能渲染 并且该侧边栏在很多页面都需要使用
1.哪个地方用就拷贝需要的代码(不推荐 有点繁琐)
2.将侧边栏制作成inclusion_tag
"""
步骤
1.在应用下创建一个名字必须叫templatetags文件夹
2.在该文件夹内创建一个任意名称的py文件
3.在该py文件内先固定写两行代码
from django import template
register = template.Library()
# 自定义过滤器
# 自定义标签
# 自定义inclusion_tag
"""
# app01.templatetags.mytag.py
from django import template
from app01.models import *
from django.db.models import Count
from django.db.models.functions import TruncMonth
register = template.Library()
# 自定义inclusion_tag
@register.inclusion_tag("left_menu.html")
def left_menu(username):
# 构造侧边栏需要的数据
user_obj = UserInfo.objects.filter(username=username).first()
blog = user_obj.blog
# 查询当前用户所有的分类及分类下的文章数
category_list = Category.objects.filter(blog=blog).annotate(count_num=Count("article__pk")).values_list(
"name",
"count_num",
"pk")
# 查询当前用户所有的标签及标签下的文章数
tag_list = Tag.objects.filter(blog=blog).annotate(count_num=Count("article__pk")).values_list(
"name",
"count_num",
"pk")
# 按照年月统计所有的文章(ORM提供的查询,此时settings.py中的TIME_ZONE和USE_TZ需要进行配置)
date_list = Article.objects.filter(blog=blog).annotate(month=TruncMonth("create_time")).values("month").annotate(
count_num=Count("pk")).values_list("month", "count_num")
return locals()
# left_menu.html
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">文章分类</h3>
</div>
<div class="panel-body">
{% for category in category_list %}
<p>
<a href="/{{ username }}/category/{{ category.2 }}/">{{ category.0 }}({{ category.1 }})</a>
</p>
{% endfor %}
</div>
</div>
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title">文章标签</h3>
</div>
<div class="panel-body">
{% for tag in tag_list %}
<p>
<a href="/{{ username }}/tag/{{ tag.2 }}/">{{ tag.0 }}({{ tag.1 }})</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 date in date_list %}
<p><a href="/{{ username }}/archive/{{ date.0|date:'Y-m' }}/">{{ date.0|date:'Y-m' }}({{ date.1 }})</a></p>
{% endfor %}
</div>
</div>
# base.html(site.html和article_detail.html所对应的视图函数都需要有username这个参数)
{% load mytag %}
{% left_menu username %}
# 视图函数
def article_detail(request, username, article_id):
# inclusion_tag的使用
# 个人站点标题需要用到blog
user_obj = UserInfo.objects.filter(username=username).first()
blog = user_obj.blog
# 先获取文章对象,查询加上名字,那么就只能通过该名字查询到其下面的文章
article_obj = Article.objects.filter(pk=article_id, blog__userinfo__username=username).first()
if not article_obj:
return render(request, "errors.html")
# 获取当前文章所有的评论内容
comment_list = Comment.objects.filter(article=article_obj)
return render(request, "article_detail.html", locals())
文章点赞点踩
"""点赞点踩样式直接copy即可,图片下载到本地使用"""
<style>
#div_digg {
float: right;
margin-bottom: 10px;
margin-right: 30px;
font-size: 12px;
125px;
text-align: center;
margin-top: 10px;
}
.diggit {
float: left;
46px;
height: 52px;
background: url('/static/img/upup.gif') no-repeat;
text-align: center;
cursor: pointer;
margin-top: 2px;
padding-top: 5px;
}
.buryit {
float: right;
margin-left: 20px;
46px;
height: 52px;
background: url('/static/img/downdown.gif') no-repeat;
text-align: center;
cursor: pointer;
margin-top: 2px;
padding-top: 5px;
}
.clear {
clear: both;
}
</style>
<div class="clearfix"> <!--下面的评论框塌陷了-->
<div id="div_digg">
<div class="diggit action">
<span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span>
</div>
<div class="buryit action">
<span class="burynum" id="bury_count">{{ article_obj.down_num }}</span>
</div>
<div class="clear"></div>
<div class="diggword" id="digg_tips" style="color: red;">
</div>
</div>
</div>
<script>
$(".action").click(function () {
let isUp = $(this).hasClass("diggit");
let $div = $(this);
// 朝后端发送Ajax请求
$.ajax({
url: "/up_or_down/",
type: "post",
data: {
"csrfmiddlewaretoken": "{{ csrf_token }}",
"article_id": "{{ article_obj.pk }}",
"is_up": isUp,
},
success: function (data) {
if (data.code === 1000) {
$("#digg_tips").text(data.msg);
let oldNum = $div.children().text(); // 文本是字符类型
$div.children().text(Number(oldNum) + 1);
} else {
$("#digg_tips").html(data.msg);
}
},
})
})
</script>
def up_or_down(request):
if request.is_ajax():
back_dic = {"code": 1000, "msg": ""}
if request.user.is_authenticated:
article_id = request.POST.get("article_id")
is_up = json.loads(request.POST.get("is_up"))
article_obj = Article.objects.filter(pk=article_id).first()
if not article_obj.blog.userinfo == request.user:
is_click = UpAndDown.objects.filter(user=request.user, article=article_obj)
if not is_click:
if is_up:
Article.objects.filter(pk=article_id).update(up_num=F("up_num")+1)
back_dic["msg"] = "点赞成功"
else:
Article.objects.filter(pk=article_id).update(down_num=F("down_num")+1)
back_dic["msg"] = "点踩成功"
UpAndDown.objects.create(
user=request.user,
article=article_obj,
is_up=is_up,
)
else:
back_dic["code"] = 1001
back_dic["msg"] = "你已经点过了!!!"
else:
back_dic["code"] = 1002
back_dic["msg"] = "不能再点了,真受不了你!!!"
else:
back_dic["code"] = 1003
back_dic["msg"] = "请<a href='/login/'>登录</a>"
return JsonResponse(back_dic)
文章评论
"""
我们先写根评论
再写子评论
点击评论按钮需要将评论框里面的内容清空
根评论有两步渲染方式
1.DOM临时渲染
2.页面刷新render渲染
子评论
点击回复按钮发生了几件事
1.评论框自动聚焦
2.将回复按钮所在的那一行评论人的姓名
@username
3.评论框内部自动换行
根评论子评论都是点击一个按钮朝后端提交数据的
parent_id
根评论子评论区别在哪?
parent_id
"""
# 评论内容是展示在文章详情页,所以在文章详情页的视图函数中,需要把当前文章的所有评论给查询出来,方便模板进行渲染数据
def comment(request):
if request.is_ajax():
back_dic = {"code": 1000, "msg": ""}
if request.method == "POST":
if request.user.is_authenticated:
article_id = request.POST.get("article_id")
content = request.POST.get("content")
parent_id = request.POST.get("parent_id")
# 直接操作评论表存储数据,需要操作两张表
with transaction.atomic():
Article.objects.filter(pk=article_id).update(comment_num=F("comment_num") + 1)
Comment.objects.create(user=request.user, article_id=article_id, content=content, parent_id=parent_id)
back_dic["msg"] = "评论成功"
else:
back_dic["code"] = 1001
back_dic["msg"] = "用户未登录"
return JsonResponse(back_dic)
{#评论楼渲染开始#}
<div>
<ul class="list-group">
{% for comment in comment_list %}
<li class="list-group-item">
<span>#{{ forloop.counter }}楼</span>
<span>{{ comment.comment_time|date:'Y-m-d h:i:s' }}</span>
<span>{{ comment.user.username }}</span>
<span class="pull-right"><a style="cursor: pointer" class="reply" username="{{ comment.user.username }}" comment_id="{{ comment.pk }}">回复</a></span>
<div>
{#判断当前评论是否是子评论,如果是需要渲染对应的根评论的姓名#}
{% if comment.parent_id %}
<p>@{{ comment.parent.user.username }}</p>
{% endif %}
{{ comment.content }}
</div>
</li>
{% endfor %}
</ul>
</div>
{#评论楼渲染结束#}
{#文章评论样式开始#}
{% if request.user.is_authenticated %}
<div>
<p><span class="glyphicon glyphicon-comment"></span>发表评论</p>
<div>
<textarea name="comment" id="id_comment" cols="60" rows="10"></textarea>
</div>
<button class="btn btn-primary" id="id_submit">提交评论</button>
<span style="color: red" id="error"></span>
</div>
{% else %}
<li><a href="{% url 'app01_register' %}">注册</a></li>
<li><a href="{% url 'app01_login' %}">登录</a></li>
{% endif %}
{#文章评论样式结束#}
<script>
let parent_id = null;
// 用户点击评论按钮,朝后端发送Ajax请求
$("#id_submit").click(function () {
// 获取用户评论的内容
let conTent = $("#id_comment").val();
// 判断当前评论是否是子评论,如果是,需要将之前手动渲染的@username去除
if (parent_id) {
// 先找到/n对应的索引值,然后利用切片,但是切片是顾头不顾尾,所以要索引+1
let indexNum = conTent.indexOf("\n") + 1;
conTent = conTent.slice(indexNum); // 将indexNum之前的所有数据切除,只保留后面的部分
}
$.ajax({
url: "/comment/",
type: "post",
data: {
"csrfmiddlewaretoken": "{{ csrf_token }}",
"article_id": "{{ article_obj.pk }}",
"content": conTent,
"parent_id": parent_id,
},
success: function (data) {
if (data.code === 1000) {
$("#error").text(data.msg);
$("#id_comment").val('');
// 临时渲染评论楼
let userName = '{{ request.user.username }}';
let temp = `
<li class="list-group-item">
<span>${userName}</span>
<div>
${conTent}
</div>
</li>
`;
// 将生成好的标签添加到ul标签内
$(".list-group").append(temp);
parent_id = null;
}
},
})
})
// 给回复按钮绑定点击事件
$(".reply").click(function () {
// 需要评论对应的评论人姓名,还需要根评论的主键值
let commentUserName = $(this).attr("username");
parent_id = $(this).attr("comment_id");
// 拼接信息塞给评论框
$("#id_comment").val("@" + commentUserName + "\n").focus();
})
</script>
后台管理页面,富文本编辑器使用
"""
kindeditor富文本编辑器,下载其文档放入static静态文件夹下即可
"""
# 用法
<textarea name="content" id="id_content" cols="30" rows="10"></textarea>
{% load static %}
<script charset="utf-8" src="{% static 'kindeditor/kindeditor-all-min.js' %}"></script>
<script>
KindEditor.ready(function(K) {
window.editor = K.create('#id_content', {
'100%', // 宽度
height: '500px', // 高度
minHeight: 300, // 最小高度
resizeType: 1, // 1代表只能上下缩放
uploadJson : '/upload_image/', // 上传图片的后端提交路径
extraFileUploadParams : { // 上传文本类型其他的参数配置
"csrfmiddlewaretoken": "{{ csrf_token }}",
},
});
});
</script>
添加文章之XSS攻击和文章简介处理
有两个需要注意的问题
1.文章的简介
不能直接切去
应该先想办法获取到当前页面的文本内容之后截取150个文本字符
2.XSS攻击
针对支持用户直接编写html代码的网址
针对用户直接书写的script标签 我们需要处理
1.注视标签内部的内容
2.直接将script删除
如何解决?
我们自己的话
针对1 后端通过正则表达式筛选
针对2 首先需要确定及获取script标签
这两步都好烦 有木有人来帮我一下
beautifulsoup模块 bs4模块
专门用来帮你处理html页面内的
该模块主要用于爬虫程序
下载千万不要下错了
pip3 install beautifulsoup4
# 模块使用
soup = BeautifulSoup(content,'html.parser')
tags = soup.find_all()
# 获取所有的标签
for tag in tags:
# print(tag.name) # 获取页面所有的标签
# 针对script标签 直接删除
if tag.name == 'script':
# 删除标签
tag.decompose()
# 文章简介
# 1 先简单暴力的直接切去content 150个字符
# desc = content[0:150]
# 2 截取文本150个
desc = soup.text[0:150]
"""
当你发现一个数据处理起来不是很方便的时候
可以考虑百度搜搜有没有现成的模块帮你完成相应的功能
"""
编辑器上传图片
"""
官方文档介绍:
上传文件:KindEditor默认提供ASP、ASP.NET、PHP、JSP上传程序,这些程序是演示程序,建议不要直接在实际项目中使用。 如果您确定直接使用本程序,使用之前请仔细确认相关安全设置。
选择程序语言:我们选择JSP
// JSP
KindEditor.ready(function(K) {
K.create('#textarea_id', {
uploadJson : '../jsp/upload_json.jsp', // 只要这一行就可以了
fileManagerJson : '../jsp/file_manager_json.jsp',
allowFileManager : true
});
});
返回格式:JSON
//成功时
{
"error" : 0,
"url" : "http://www.example.com/path/to/file.ext"
}
//失败时
{
"error" : 1,
"message" : "错误信息"
}
"""
def upload_image(request):
back_dic = {"error": 0}
if request.method == "POST":
file_obj = request.FILES.get("imgFile")
# 手动拼接存储文件的路径
file_dir = os.path.join(settings.BASE_DIR, "media", "article_img")
# 先判断当前文件夹是否存在,不存在则创建
if not os.path.isdir(file_dir):
os.mkdir(file_dir)
# 拼接图片的完整路径
file_path = os.path.join(file_dir, file_obj.name)
with open(file_path, "wb") as f:
for line in file_obj:
f.write(line)
# 这一步路径如果直接使用file_path,那么路径会变成bbs_project/media/article_img/...
back_dic["url"] = "/media/article_img/%s" % file_obj.name
return JsonResponse(back_dic)
用户修改头像
@login_required
def set_avatar(request):
user_obj = UserInfo.objects.filter(username=request.user.username).first()
blog = user_obj.blog
username = request.user.username
if request.method == "POST":
file_obj = request.FILES.get("avatar")
# 修改用户头像,如果直接update,那么存储的时候不会有自动加前缀avatar,只能手动对该字段更改然后save,或者手动加前缀avatar
user_obj.avatar = file_obj
user_obj.save()
return redirect("/home/")
return render(request, "set_avatar.html", locals())
编辑文章
"""
自己在实现的过程中出现了一点问题,所以特地记录一下
"""
{% extends 'backend/base.html' %}
{% block home %}
<h3>编辑文章</h3>
<form action="" method="post" enctype="multipart/form-data">
{% csrf_token %}
<p>标题</p>
<div>
<input type="text" name="title" class="form-control" id="id_title">
</div>
<br>
<p>内容</p>
<div>
<textarea name="content" id="id_content" cols="30" rows="10"></textarea>
</div>
<br>
<p>分类</p>
<div>
{% for category in category_list %}
{% if category == article.category %}
<input type="radio" value="{{ category.pk }}" name="category" checked>{{ category.name }}
{% else %}
<input type="radio" value="{{ category.pk }}" name="category">{{ category.name }}
{% endif %}
{% endfor %}
</div>
<br>
<p>标签</p>
<div>
{% for tag in tag_list %}
{% if tag in article.tags.all %}
<input type="checkbox" value="{{ tag.pk }}" name="tag" checked>{{ tag.name }}
{% else %}
<input type="checkbox" value="{{ tag.pk }}" name="tag">{{ tag.name }}
{% endif %}
{% endfor %}
</div>
<input type="submit" class="btn btn-danger">
</form>
<div style="display: none" id="d1">
{#存储文章的内容#}
{{ article.content|safe }}
</div>
{% endblock %}
{% block js %}
{% load static %}
<script charset="utf-8" src="{% static 'kindeditor/kindeditor-all-min.js' %}"></script>
<script>
KindEditor.ready(function(K) {
window.editor = K.create('#id_content', {
'100%',
height: '500px',
minHeight: 300,
resizeType: 1,
uploadJson : '/upload_image/', // 上传图片的后端提交路径
extraFileUploadParams : {
"csrfmiddlewaretoken": "{{ csrf_token }}",
},
});
});
$("#id_title").val('{{ article.title }}');
// 获取到隐藏div中的html内容,即文章内容
let html = $("#d1").html();
// 赋值给textarea标签
$("#id_content").val(html);
// 同步到编辑器,这个语法在官方文档有写
editor.html(html);
</script>
{% endblock %}
# 后端实现逻辑和添加文章一样,只不过还需要把当前文章的标签和分类查出来提供给前端好进行标签的渲染