BBS项目创作流程
【零】完整文件
- gitee仓库
- BBS/BBS1.0/BlogBasedSystem · Lea4ning/DjangoObject - 码云 - 开源中国 (gitee.com)
【一】项目基本配置
【1】所需模块
asgiref==3.7.2
beautifulsoup4==4.12.3
certifi==2024.2.2
charset-normalizer==3.3.2
Django==3.2.12
fake-useragent==1.5.1
idna==3.6
lxml==5.1.0
mysqlclient==2.2.4
pillow==10.2.0
pytz==2024.1
requests==2.31.0
soupsieve==2.5
sqlparse==0.4.4
typing_extensions==4.10.0
tzdata==2024.1
urllib3==2.2.1
【2】基础搭建
# settings.py# 注册app
INSTALLED_APPS =['...']
# 配置数据库
DATABASES = {'default': {'ENGINE': 'django.db.backends.mysql',# 库名需要提前在mysql中注册'NAME': 'blog_based_system','HOST': '127.0.0.1','PORT': 3306,'USER': 'root','PASSWORD': 'xxx','CHARSET': 'utf8mb4'}
}
# 修改时区
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
# 配置静态文件路径
STATICFILES_DIRS = [os.path.join()]# 配置用户表
AUTH_USER_MODEL = '指定用户表'# 配置媒体文件路径
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR,'media')
【二】建表
【1】对数据表进行分析
- 用户表(UserInfo)
- 继承
AbstractUser
表 - 拓展字段
- 继承
字段名 | 类型 | 注释 |
---|---|---|
phone | BigIntegerField | 电话 |
avatar | FileField | 头像链接 |
create_time | DateField | 创建时间 |
blog | OneToOneField(to="Blog") | 外键字段,一对一,个人站点表 |
- 个人站点表(Blog)
字段名 | 类型 | 注释 |
---|---|---|
site_name | CharField | 站点名称 |
site_title | CharField | 站点标题 |
site_theme | CharField | 站点样式 |
- 文章分类表(Category)
字段名 | 类型 | 注释 |
---|---|---|
name | CharField | 分类名 |
blog | ForeignKey(to="Blog") | 外键字段,一对多,个人站点 |
- 标签表(Tag)
字段名 | 类型 | 注释 |
---|---|---|
name | CharField | 标签名 |
- 文章表(Article)
字段名 | 类型 | 注释 |
---|---|---|
title | CharField | 文章标题 |
desc | CharField | 文章摘要/文章简介 |
content | TextField | 文章内容 |
create_time | DateField | 发布时间 |
up_num | BigIntegerField | 点赞数 |
down_num | BigIntegerField | 点踩数 |
comment_num | BigIntegerField | 评论数 |
blog | ForeignKey(to="Blog") | 外键字段,一对多,个人站点 |
category | ForeignKey(to="Category") | 外键字段,一对多,文章分类 |
tags | ManyToManyField(to="Tag") | 外键字段,多对多,文章标签 |
- 点赞点踩表(UpAndDown)
- 用来记录哪个用户对哪篇文章点了赞还是点了踩
字段名 | 类型 | 注释 |
---|---|---|
user | ForeignKey(to="UserInfo") | 用户主键值 |
article | ForeignKey(to="Article") | 文章主键值 |
is_up | BooleanField() | 是否点赞 |
- 评论表(Comment)
- 用来记录哪个用户给哪篇文章写了哪些评论内容
字段名 | 类型 | 注释 |
---|---|---|
user | ForeignKey(to='UserInfo') | 用户主键值 |
article | ForeignKey(to="Article") | 文章主键值 |
content | CharField() | 评论内容 |
comment_time | DateTimeField | 评论时间 |
parent | ForeignKey(to="self",null=True) | 自关联 |
【2】建表注意事项
【2.1】基于AbstractUser
表创建UserInfo
表
注意,基于系统表创建用户表需要在执行数据迁移前,django默认用户表表名为auth_user
- 用户信息表基于【
from django.contrib.auth.models import AbstractUser
】创建 - 注意要在
settings.py
文件中配置AUTH_USER_MODEL='指定用户表'
- 这样创建的用户表可以被django管理后台识别,可以通过该表中的数据登录django后台
【2.2】公共表CommentModel
- 因为很多表可能需要用到创建时间和更新时间的字段,可以创建一个公共表来快捷的注册这两个字段
- 当模型表继承公共表时,如果在创建公共表时,公共表继承了
models.Model
那么继承公共表的可以不用继承models.Model
,
class CommonModel(models.Model):create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')update_time = models.DateTimeField(auto_now=True, verbose_name='最后更新时间')class Meta():# 将公共表定义成抽象表# 只能继承,无法实例化abstract = True
【2.3】在建表时使用class Meta
和def __str__(self)
class Table(models.Model):...class Meta:db_table = '指定在数据库中的表名' # 数据库建表时默认使用【应用程序名+表名小写】verbose_name_plural = '为该表起别名'def __str__(self):return '可以返回一些实例化模型表对象时的信息'# 比如用户对象时,可以返回【self.name】,这样在后端查看时,可以直接显示用户姓名
【2.4】勤使用三引号作注释
【3】将数据库表注册至管理员后台
- 在django管理员后台可以查看并操作数据库
# admin.py # 每一个应用下都有一个admin文件 # 根据分表创建注册即可from django.contrib import admin# Register your models here.
from .models import 表@admin.register(表)
class 表名(admin.ModelAdmin):list_display = ['注册想要显示在后台的字段']
- 注意事项
- 在后台中,多对多字段是没办法显示的,所以多对多的字段注册不进去
- 注册在后台的字段,显示的是
verbose_name
属性的值,所以建表时,可以指定值
【4】使用Navicat逆向数据库至模型
- 在Navicat中【右键数据库】---- 【 逆向数据库至模型】
- 模型参考
【前置】公共功能
【1】CommonResponse : 返回json格式的响应
- 通用的返回
jsonresponse
格式数据
from django.http import JsonResponsedef CommonResponse(code=200, msg=None, **kwargs):'''为了在ajax请求时,方便的返回响应数据:param code: 响应状态码:param msg: 响应信息:param kwargs: 其他可能会有的关键字参数:return: JsonResponse'''if msg:data = {'code': code, 'msg': msg}else:data = {'code': code}if kwargs:data.update(kwargs)return JsonResponse(data)
【2】CommonPaper : 分页器
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: 最多显示的页码个数"""try:current_page = int(current_page)except Exception as e:current_page = 1if current_page < 1:current_page = 1self.all_count = all_countself.per_page_num = per_page_num# 总页码all_pager, tmp = divmod(all_count, per_page_num)if tmp:all_pager += 1self.all_pager = all_pagerif current_page > all_pager:current_page = all_pagerself.current_page = current_pageself.pager_count = pager_countself.pager_count_half = int((pager_count - 1) / 2)@propertydef start(self):return (self.current_page - 1) * self.per_page_num@propertydef end(self):return self.current_page * self.per_page_numdef page_html(self):# 如果总页码 < 11个:if self.all_pager <= self.pager_count:pager_start = 1pager_end = self.all_pager + 1# 总页码 > 11else:# 当前页如果<=页面上最多显示11/2个页码if self.current_page <= self.pager_count_half:pager_start = 1pager_end = self.pager_count + 1# 当前页大于5else:# 页码翻到最后if (self.current_page + self.pager_count_half) > self.all_pager:pager_end = self.all_pager + 1pager_start = self.all_pager - self.pager_count + 1else:pager_start = self.current_page - self.pager_count_halfpager_end = self.current_page + self.pager_count_half + 1page_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)
【3】CommonVerifyCode : 图形验证码
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import random
from io import BytesIOdef rgb_number():# rgb_tuple = [random.randint(0, 255) for i in range(3)]return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)# 创建图片验证码
def create_verify_code(img_type="RGB", img_size: tuple = (115, 34), img_rgb_number: tuple = None):if not img_rgb_number:img_rgb_number = rgb_number()# 利用 Image 对象定义一个图片文件参数img_obj = Image.new(mode=img_type, size=img_size, color=img_rgb_number)img_obj.filter(ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3))# 利用 ImageDraw 对象产生一个画笔对象img_draw = ImageDraw.Draw(img_obj)# 利用 ImageFont 对象定义验证码字体参数(字体样式,字体大小)img_font = ImageFont.truetype('static/font/汉仪晴空体简.ttf', 30)# 创建随机验证码(数字 + 字母)code = ''for i in range(4):string_list = [str(random.randint(0, 9)),chr(random.randint(65, 90)),chr(random.randint(97, 122))]temp = random.choice(string_list)# 借助画笔对象依次写入图片数据((x偏移值,y偏移值),写入内容,字体颜色,字体样式)img_draw.text((i * 25 + 10, 0), temp, fill=rgb_number(), font=img_font)# 拼接随机字符串code += temp# 随机验证码在登录的视图函数中需要比对,所以需要找地方存起来,便于其他函数调用# 生成 IO 对象 操作字节流数据io_obj = BytesIO()# 利用 Image 对象保存图片数据成指定格式img_obj.save(io_obj, "png")# 利用 IO 对象读取保存的图片字节数据img_data = io_obj.getvalue()# 返回生成的随机验证码和验证码的图片二进制数据return code, img_data
【三】用户功能
【1】用户注册
【1.1】前端
- 需要渲染错误信息时,你的错误信息需要与
{{ form }}
在同一个块级标签中,如果分成两个,可能在渲染时无法找到
-
渲染头像小技巧
-
实时渲染
-
使用
form.serializeArray()
获得form表单中所有的数据
-
渲染错误信息
-
报错信息一直显示着比较难看,所以当用户将鼠标回到input框进行修改时,将报错信息以及错误样式取消
- 需要注意,以上操作均需要在事件监控的前提下进行
-
【注】使用箭头函数时
this
就失效了,就无法定位到当前对象了
【1.2】后端
- 后端基本上没有到很巧妙的技术,都是基础技术,直接放上代码
- 【注】由于用户信息与个人站点绑定,所以在录入信息时,将站点信息一起录入
def register(request):register_form = RegisterForm()# 判断是否时post且时ajax传入的if request.method == 'POST' and request.is_ajax():kv_data = request.POST# 得到普通的键值对数据register_form = RegisterForm(kv_data)# 对数据进行清洗if not register_form.is_valid():# 如果有错误,将错误信息传回前端return CommonResponse(code=300, errors=register_form.errors)kv_data = register_form.cleaned_data# 获取到清洗过的数据# 并讲在数据库中没有的字段删除掉kv_data.pop('confirm_pwd')blog_title = kv_data.pop('blog_title')# 获取文件对象avatar_data = request.FILES.get('avatar')theme_data = request.FILES.get('theme')if not theme_data:blog_obj = Blog.objects.create(name=kv_data.get('username'), title=blog_title)else:blog_obj = Blog.objects.create(name=kv_data.get('username'), title=blog_title, theme=theme_data)# 创建用户,可以通过解包将字典中的键值对传回数据库if avatar_data:# 如何上传了头像,使用对应的头像user_obj = UserInfo.objects.create_user(**kv_data, blog=blog_obj, avatar=avatar_data)else:# 如果没有上传头像,使用默认头像user_obj = UserInfo.objects.create_user(**kv_data, blog=blog_obj)# 注册成功,向前端返回信息return CommonResponse(code=200, msg=f"{kv_data.get('username')} 注册成功")return render(request, 'register.html', locals())
【2】用户登录
- 登录就是基于auth模块对用户名和密码进行校验
'''使用了图片验证码,没有使用forms组件,使用的是layui表单'''
def login(request):'''没用forms组件'''# login1_obj = LoginForm()if request.is_ajax() and request.method == 'POST':data = request.POSTusername = data.get('username')password = data.get('password')captcha = data.get('captcha')if captcha.upper() != request.session.get('captcha').upper():return CommonResponse(code=300, errors='验证码错误')user_obj = auth.authenticate(username=username, password=password)if not user_obj:return CommonResponse(code=300, errors='密码输入错误')auth.login(request, user_obj)return CommonResponse(code=200, msg=f'{username}登录成功')return render(request, 'Login_test.html', locals())
【3】用户注销
- 基于auth模块的注销功能
@login_required
def logout(request):auth.logout(request)url = reverse('home')return redirect(to=url)
【4】修改密码 [ 使用模态框跳转 ]
# views.pydef editPassword(request):if request.is_ajax() and request.method == 'POST':data = request.POST# username = request.user.username# 获取数据old_password = data.get('old_password')new_password = data.get('new_password')captcha = data.get('captcha')# 条件判断if not all([old_password, new_password, captcha]):return CommonResponse(code=100, errors='请补全参数')if captcha.upper() != request.session.get('captcha').upper():return CommonResponse(code=300, errors='验证码输入错误')is_pwd = request.user.check_password(old_password)if not is_pwd:return CommonResponse(code=400, errors='原密码输入错误')if len(new_password) < 5:return CommonResponse(code=401, errors='密码至少5位')# 保存修改数据request.user.set_password(new_password)request.user.save()# 将登录状态退出auth.logout(request)return CommonResponse(code=200, msg='密码修改成功')
- 导航栏菜单
- 关键参数:
data-toggle
和data-target
,执行模态框
- 关键参数:
<li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"aria-expanded="false">{{ request.user.username }} <span class="caret"></span></a><ul class="dropdown-menu"><li><a href="#" data-toggle="modal" data-target="#editPwdModal">修改密码</a></li><li><a href="#">修改头像</a></li><li><a href="#">修改信息</a></li><li role="separator" class="divider"></li><li><a href="{% url 'user:logout' %}">退出登录</a></li></ul>
</li>
- 模态框
<!-- 修改密码模态框 -->
<div class="modal fade" id="editPwdModal" 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" id="myModalLabel">修改密码</h4></div><form id="editPwdForm">{% csrf_token %}<div class="modal-body"><div class="form-group"> {# 用户名显示 #}<label for="username">用户名 :</label><input type="text" id="username" name="username" value="{{ request.user.username }}" disabledclass="form-control"></div><div class="form-group"> {# 旧密码校验 #}<label for="old_password">原密码 :</label><input type="password" id="old_password" name="old_password" class="form-control"></div><div class="form-group"> {# 新密码校验 #}<label for="new_password">新密码 :</label><input type="password" id="new_password" name="new_password" class="form-control"></div><div class="form-group"><label for="id_captcha">验证码</label><div class="row"><div class="col-md-9"><input type="text" id="id_captcha" class="form-control"name="captcha"></div><div class="col-md-3"><img src="{% url 'user:verify_code' %}" alt="图片验证码"id="captcha_img"onclick="this.src='{% url 'user:verify_code' %}'+'?t='+ new Date().getTime();"></div></div></div></div><div class="modal-footer"><button type="button" class="btn btn-default" data-dismiss="modal">关闭</button><input type="button" class="btn btn-primary" id="editPwdSave" value="保存修改"></div></form></div></div>
</div><!-- 修改密码模态框结束 -->
【四】站点搭建
- 前端框架主要使用
bootstrap V3
【1】主页导航栏搭建
【1.1】根据登录状态显示不同页面
-
通过后端
locals()
传递上下文数据,可以将request对象
传递给前端 -
可以通过
request.use
等操作,判断属性和获取属性
<!-- 用户登录页面 -->
{% if request.user.is_authenticated %} <!-- 判断用户登录状态 -->
<!-- 用户登录显示用户头像,用户名 -->
<li><img src="/media/{{ request.user.avatar }}/" alt="头像走丢了"style="height: 50px;width: 50px;align-items: center" class="img-circle"></li>
<li class="dropdown"><!-- 用户名可以设置下拉菜单提供用户操作 --><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"aria-expanded="false">{{ request.user.username }} <span class="caret"></span></a><ul class="dropdown-menu"><li><a href="#">Action</a></li><li><a href="#">Another action</a></li><li><a href="#">Something else here</a></li><li role="separator" class="divider"></li><li><a href="{% url 'user:logout' %}">退出登录</a></li></ul>
</li>
{% else %} <!-- 用户未登录页面 -->
<li><a href="{% url 'user:register' %}">注册</a></li>
<li><a href="{% url 'user:login' %}">登录</a></li>
{% endif %}
【1.2】搜索栏携带数据去百度搜索
- 表单
<form class="navbar-form navbar-left" id="form_search"> <!-- 搜索表单 --><div class="form-group"><input type="text" class="form-control" placeholder="Search" id="input_search"></div><button type="submit" class="btn btn-default">百度一下</button>
</form> <!-- 搜索表单结束 -->
- js代码
<script>$(document).ready(function () {// 声明搜索函数let func_search = function search() {let txt = $("#input_search").val();// 拼接地址let next_url = 'https://www.baidu.com/s' + '?wd=' + txtwindow.open(next_url, '_blank')}; // 声明函数结束// 阻止搜索的form表单提交 执行函数$("#form_search").submit(function (event) {event.preventDefault()func_search()}) // 执行函数结束} // ready(){} end) // ready end
</script>
【2】使用自定义标签生成广告
templatetags
:存放自定义标签函数的文件夹blog_inclusion_tags.py
:自定义标签函数template/inclusion_tags
:存放生成网页标签的内容home.html
:使用自定义标签的页面
'''blog_inclusion_tags.py'''# 注册自定义标签
from django import templateregister = template.Library()# .register.inclusion_tag('生成网页标签的网页')
@register.inclusion_tag('inclusion_tags/adv_label.html')
def adv_main():# 网页标签需要使用的数据adv_obj = Advertisement.objects.all().order_by('pk')return locals()
<!-- template/inclusion_tags/adv_label.html -->{% load static %}{% for adv in adv_obj %}<div class="adv_block" style="margin-bottom: 30px"><div class="pull-right" style="fill: none"><a class="btn_close_adv"><span class="glyphicon glyphicon-remove"></span></a></div><div class="thumbnail"><img src="/static/{{ adv.img }}/" alt="" style="height: 300px;width: 200px"><div class="caption"><h3>{{ adv.title }}</h3><p>{{ adv.content|truncatechars:10 }}...</p> <!-- 正常英文时区下使用truncate省略部分可以自动显示...,中文时区无法显示,所以手动加 --><p><a href="#" class="btn btn-danger" role="button">查看详细</a></div></div></div>
{% endfor %}<script>$('.btn_close_adv').click(function () {$(this).closest('.adv_block').hide()})
</script>
<body><div class="col-md-2"><!-- 加载标签函数文件 -->{% load blog_inclusion_tags %}<!-- 使用标签函数 -->{% adv_main [参数] %}
</div> <!-- 左侧广告栏 -->
</body>
【五】文章界面搭建
【1】新建文章
【1.1】文章编辑器(kindeditor)
- 文档 - KindEditor - 在线HTML编辑器
<body><textarea id="editor_id" name="content" style="width:700px;height:300px;"></textarea>
</body><script>KindEditor.ready(function (K) {// K.create('指定区域',{参数})window.editor = K.create('#editor_id', {// resizeType : 可改变模式 2时可以拖动改变宽度和高度,1时只能改变高度,0时不能拖动resizeType: 1,// themeType : 编辑器主题 【需要导入对应的css文件】themeType: 'simple',// 自动调整高度autoHeight: true,// 上传文件时,上传的url地址uploadJson: '{% url 'article:articleImg' %}',// 可以携带的额外的参数 为了通过csrf验证extraFileUploadParams: {'csrfmiddlewaretoken': '{{ csrf_token }}'}});});
</script>
【1.2】文章编辑器上传文件
- 通过编辑器上传文件时需要在后端对图片文件进行处理
def articleImg(request):file_obj = request.FILES.get('imgFile')# 设置保存到的文件指定位置img_folder_path = os.path.join(settings.BASE_DIR, 'media', 'article_img')# 确保文件夹存在os.makedirs(img_folder_path, exist_ok=True)# 拼接图片存放路径img_path = os.path.join(img_folder_path, file_obj.name)try:# 将图片保存至指定路径 # 一般是media文件夹,保证网页可以通过url获取图片with open(img_path, 'wb') as fp:for i in file_obj.chunks():fp.write(i)# 拼接并返回图片urlurl = f'/media/article_img/{file_obj.name}/'# 返回的参数格式有讲究的# 需要是json格式 # 参数0代表上传成功,参数1代表失败# 成功返回url # 失败返回message # 参数名称必须是指定的两个return CommonResponse(error=0, url=url)except Exception as e:message = f'发生了一些意外{e}'return CommonResponse(error=1, message=message)
【1.2.1】iframe验证错误,导致上传卡住
- 如果函数正确返回了url,但是卡在一直上传,可能是因为django的
django.middleware.clickjacking.XFrameOptionsMiddleware
中间价,对iframe
进行了限制
- iframe中可能会设置恶意代码,所以django做了一层验证
- 我们可以先将中间件注释掉,我们手动对文章内容去除恶意代码
【1.2.2】对文章内容进行处理
- 如果在输入的网页内容中,提交了
js动作
,而js动作可能会导致你的信息泄漏等信息安全事故,也就是xss攻击 - 为了预防xss攻击,我们需要将js动作删除或更改标签使其无法触发
- 通过
BeautifulSoup
解析网页文档,找到script
标签并将其删除或更改
from bs4 import BeautifulSoupdef __analysis_content(content):# 解析HTMLsoup = BeautifulSoup(content, 'lxml')desc = soup.text[:50]# 找到js标签防止xss攻击original_paragraph = soup.script# 直接删除script标签块 或 使用下面的替换# original_paragraph.replace_with('')if not original_paragraph:# 如果文章中没有写js代码就可以直接返回return desc, content# 创建一个新的段落new_paragraph = soup.new_tag("p")new_paragraph.string = str(soup.script)# 用新的段落替换原始段落original_paragraph.replace_with(new_paragraph)content = soup.prettify()return desc, content
【2】修改文章
【2.1】使用get+post方法重新渲染修改文章网页
''' 在gitee中的bbs1.0版本中 使用的是重新渲染修改文章的网页 '''
# 会比较麻烦,个人认为两次提交ajax效果可能会好一些'''
通过调用指定视图函数,将指定文章id传递给后端,后端将文章数据渲染到前端
代码就是前端使用form表单
'''
【2.2】使用两次ajax提交事件,返回渲染数据
''' 在修改广告时,使用了两次ajax事件 '''
# 第一次ajax请求通过get请求将文章id传到后端,后端将数据返回,前端渲染数据
# 第二次ajax请求将修改后的数据返回
<!-- 使用了layui的form表单,所以提交语法不一致,不过思路是一致的 -->
<script>
$('.btn_edit_adv').click(function () {let pk = $(this).attr('value');$.ajax({url: '{% url 'backend:editAdvertisement'%}',// 通过get请求将pk数据传给后端type: 'get',contentType: 'application/json',data: {'pk': pk},// 通过回调函数将返回的指定数据渲染到指定位置success: function (res) {$('#oldTitle').attr('value', res.title);$('#newTitle').attr('value', res.title);$('#oldTitleInput').attr('value', res.title);$('#adv_content').text(res.content);$('#newPhone').attr('value', res.phone);$('#old_adv').attr('src', '/media/' + res.img);$('#img_new_adv').attr('src', '/media/' + res.img);}})}))// 第二次ajax提交layui.use(['form'], function () {let form = layui.form;// 提交事件form.on('submit(btn_edit_adv)', function () {let formData = new FormData;$.each($('#editAdvForm').serializeArray(), function (index, data_dict) {console.log(data_dict)formData.append(data_dict.name, data_dict.value)})formData.append('new_adv_img', $('#new_img_adv')[0].files[0])$.ajax({url: '{% url 'backend:editAdvertisement' %}',type: 'post',data: formData,processData: false,contentType: false,success: function (res) {if (res.code===200){window.location.href='{% url 'backend:manage' 'advertisement' %}'}}})return false; // 阻止默认 form 跳转});});
</script>
【3】渲染上一篇文章及下一篇文章
def article_detail(request, username, pk):# 过滤当前用户的所有文章_pk_list_all = Article.objects.filter(blog__userinfo__username=username).values('pk').all()# 使用列表生成式 获得所有的文章id_pk_list = [d1.get('pk') for d1 in _pk_list_all]if pk in _pk_list:# 如果文章在id列表中执行以下代码# 获得当前文章的id在所有id列表中索引位置index = _pk_list.index(pk)if index == 0:# index =0 表示显示的是当前用户的第一篇文章index = 1elif index == len(_pk_list) - 1:# 如果索引 = 总长度减1 也就意味着显示的是最后一篇文章# 将索引-1 用来展示最后一篇文章index -= 1# 显示上一篇和下一篇文章last_article_obj = Article.objects.get(pk=_pk_list[index - 1])if len(_pk_list) == 1:# 如果总共只有一篇文章,那么下一篇还是第一篇next_article_obj = Article.objects.get(pk=_pk_list[index - 1])else:next_article_obj = Article.objects.get(pk=_pk_list[index + 1])article_now = Article.objects.get(pk=pk)# blog_obj为了保留样式blog_obj = Blog.objects.get(userinfo__username=username)# 文章总数article_data_all = Article.objects.filter(blog=blog_obj)# 点赞总数up_gross = article_data_all.aggregate(up_gross=Sum('up_count'))['up_gross']# 点踩总数down_gross = article_data_all.aggregate(down_gross=Sum('down_count'))['down_gross']# 评论总数comment_gross = article_data_all.aggregate(comment_gross=Sum('comment_count'))['comment_gross']return render(request, 'article_detail.html', locals())
【4】文章点赞和点踩逻辑
@login_required
def edit_article_like(request):if request.is_ajax() and request.method == 'POST':user_obj = request.userif not user_obj.is_authenticated:return CommonResponse(code=400, errors='请先登录')# 获取ajax发送的数据data = request.POSTarticle_id = int(data.get('article_id'))is_up = True if data.get('tag') == 'true' else Falsestate = '点赞' if is_up else '点踩'article_obj = Article.objects.get(pk=article_id) # 文章对象up_down_obj = UpOrDown.objects.filter(user=user_obj, article=article_obj).first() # 点赞或点踩对象if article_obj.blog.userinfo.username == user_obj.username:return CommonResponse(code=402, errors='不能给自己点赞或点踩!')# 进行条件判断if up_down_obj:# 原表中有则进行是否一致判断if up_down_obj.is_up == is_up:# 一致返回不可重复提交return CommonResponse(code=403, errors=f'当前用户已{state},不可重复{state}')else:# 不一致进行更新up_down_obj.is_up = is_upup_down_obj.save()if is_up:article_obj.up_count += 1article_obj.down_count -= 1else:article_obj.up_count -= 1article_obj.down_count += 1else:# 如果原表中没有值,新增值UpOrDown.objects.create(user=user_obj, article=article_obj, is_up=is_up)if is_up:article_obj.up_count += 1else:article_obj.down_count += 1# 更新文章对象的修改操作article_obj.save()return CommonResponse(code=200, msg=f'{state}成功')
<body><!-- 如果用户已登陆允许点赞 -->{% if request.user.is_authenticated %}<div id="div_digg" style="margin-top: 50px;"><div class="diggit" onclick="votePost({{ article_now.pk }},'Digg')"><span class="diggnum" id="digg_count">{{ article_now.up_count }}</span></div><div class="buryit" onclick="votePost({{ article_now.pk }},'Bury')"><span class="burynum" id="bury_count">{{ article_now.down_count }}</span></div><div class="clear"></div><div class="diggword" id="diggit_tips"></div></div><!-- 没登陆渲染让其先登录 -->{% else %}<div>请先<a href="{% url 'article:edit_article_like' %}">登录</a>,登陆后后即可点赞或点踩</div>{% endif %}
</body><script><!-- 点赞点踩动作 -->function votePost(pk, tag) {let data = {'article_id': pk, 'tag': 1 ? tag === 'Digg' : 0, 'csrfmiddlewaretoken': '{{ csrf_token }}'};$.ajax({url: '{% url 'article:edit_article_like' %}',type: 'post',data: data,success: (res) => {if (res.code === 200) {$('#diggit_tips').text(res.msg);} else {$('#diggit_tips').text(res.errors);}// 设置1秒后刷新状态setTimeout(function () {// 局部刷新$('#div_digg').load(location.href + ' #div_digg');$('#data_gross').load(location.href + ' #data_gross');}, 1000)}});}</script>
【5】渲染文章评论
- 可以使用
inclusiontag
渲染评论的内容 - 主要逻辑其实就是将主评论和子评论通过元组放置在一起
(主评论的queryset对象,该主评论对应的子评论queryset对象)
- 在前端通过
tuple.索引
,分别获取到主评论和子评论
<body>{% if request.user.is_authenticated %}<!-- 渲染自定义标签 -->{% load article_inclusion_tags %}{% comment_flat blog_obj article_now %}<!-- 未登录让其登录 -->{% else %}<div>请先<a href="{% url 'article:comment' %}">登录</a>,登录后查看评论、发表评论</div>{% endif %}
</body>
自定义标签.py
文件
# article_inclusion_tags.pyfrom django import template@register.inclusion_tag('article_inclusion_tags/comment_inclusion.html')
def comment_flat(blog_obj, article_now):# 获取所有主评论_main_comments = Comment.objects.filter(article=article_now).filter(parent=None)comment_data_list = []for father_comment in _main_comments:# 获取所有主评论及其对应的子评论child_comment = Comment.objects.filter(parent=father_comment.id)# 将主评论和子评论作为元组传入 (主评论对象,子评论queryset)comment_data_list.append((father_comment, child_comment))return locals()
生成网页标签.html
文件
<body>{% for comment_data in comment_data_list %}<div class="panel"><div class="panel-heading" role="tab" id="headingOne"><h6 class="panel-title">#{{ forloop.counter }}楼 {{ comment_data.0.create_time|date:'Y-m-d H:i:s' }} <a href="{% url 'blog:site' comment_data.0.user.username %}">{{ comment_data.0.user.username }}</a><p class="pull-right opacity50"><a class="btn_reply" href="#input_comment"reply_user="{{ comment_data.0.user.username }}"parent_id="{{ comment_data.0.id }}">回复</a></p><p class="margin_L30">{{ comment_data.0.content }}</p> <!-- 主评论内容 -->{% if comment_data.1.count %}<a class="collapsed pull-right opacity50" role="button" data-toggle="collapse"data-parent="#accordion"href="#comment_collapse_{{ comment_data.0.pk }}"aria-expanded="false" aria-controls="comment_collapse_{{ comment_data.0.pk }}">其他评论【{{ comment_data.1.count }}】</a>{% endif %}</h6></div><div id="comment_collapse_{{ comment_data.0.pk }}" class="panel-collapse collapse"role="tabpanel"aria-labelledby="headingOne"><div class="panel-body"><div class="media-body">{% if comment_data.1.exists %}{% for child_comment in comment_data.1 %}<p class="margin_L30">{{ child_comment.create_time|date:'Y-m-d H:i:s' }} <a href="{% url 'blog:site' child_comment.user.username %}">{{ child_comment.user.username }}</a>{% if child_comment.user.username == comment_data.0.user.username %}【楼主】{% endif %}</p><p class="pull-right opacity50"><a class="btn_reply" href="#input_comment"reply_user="{{ child_comment.user.username }}"parent_id="{{ comment_data.0.id }}">回复</a></p><p class="margin_L30">{{ child_comment.content }}</p> <!-- 子评论内容 --><hr>{% endfor %}{% endif %}</div></div></div></div>{% endfor %}
</body>
【6】回复评论自动添加回复的用户
- 思考逻辑就是,为回复按钮添加属性
<!-- 评论区域的form表单代码就不贴了,比较简单 -->
<script>// 提前声明一个主评论id,如果有主评论,那么设置值,如果没有默认就是nulllet parentId = null$(document).ready($('#btn_comment').click(function () {let content = $('#input_comment').val();$.ajax({url: '{% url 'article:comment' %}',type: 'post',data: {'article_id':{{ article_now.pk }},'content': content,// 将主评论id带入到ajax数据中'parent_id': parentId,'csrfmiddlewaretoken': '{{ csrf_token }}'},success: function (res) {if (res.code === 200) {alert(res.msg);// 局部刷新评论区{#$('#comment_flat').load(location.href + ' #comment_flat');#}// 局部刷新不能多次提交,直接整个页面刷新location.reload()}}})}),$('.btn_reply').click(function () {let reply_user = $(this).attr('reply_user');let parent_id = $(this).attr('parent_Id');$('#input_comment').text('@' + reply_user + '\n');// 回复时设置主评论idparentId = parent_id}))
</script>
【六】博客后台
【1】标签切换
- 使用的是bootstrap中的
Togglable tabs
- JavaScript 插件 · Bootstrap v3 中文文档 | Bootstrap 中文网 (bootcss.com)
<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></ul><!-- Tab panes --><div class="tab-content"><div role="tabpanel" class="tab-pane active" id="home">...</div></div>
</div>
- 后端将数据传入,前端根据数据渲染即可,具体代码可以在gitee看,当前展示一下效果
【2】回收站
- 实现逻辑就是在数据库中增加了一个是否删除的字段
def recycleBin(request):'''回收站'''blog_obj = Blog.objects.get(userinfo=request.user)# 过滤出字段is_delete为true的文章article_all = Article.objects.filter(blog__userinfo=request.user).filter(is_delete=True)return render(request, 'recycle_bin.html', locals())def revoke(request, pk):'''撤销删除'''Article.objects.filter(pk=pk).update(is_delete=False)return redirect('backend:recycle')
【七】排错技巧
【1】forbidden
-
排查是否有
{% csrf_token% }
-
排查前端是否成功将数据传回后端
- 一个是查看获取表单数据时,是否正确的获取到了csrf的值
- 另一个是查看数据类型是什么?是否可以被django读取
-
将csrf中间件注释掉,看看后端有没有收到数据
-
使用layui的form组件传值的时候,传回的值好像是obj对象,而不是字典,所以会forbidden,因为django没办法搜索到
【2】参数传递失败
- 在前端时,尽量不要直接使用变量,尽量使用一个公用的对象去反向查询获取值
- 因为在模板继承时,变量极有可能传值失败,而且变量的值有些时候你不清楚获取的是哪一个
- 例如:在站点中,需要username参数,这个username参数,在site视图函数中可以通过url中的路径获得,而其他视图函数不一定能通过url获得,所以需要考虑其他方式获得
- 基本上每一个试图函数都需要使用的参数是
blog_obj
,因为很多功能依赖于站点 - 这样的话,如果需要username,我就可以通过
blog_obj.userinfo.username
获得 - 如果依赖于登录用的功能,就可以直接通过
request.user.username
获得
- 基本上每一个试图函数都需要使用的参数是