BBS项目创作流程
BBS项目创作流程
【零】完整文件
【一】项目基本配置
【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 JsonResponse
def 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 = 1
if current_page < 1:
current_page = 1
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
if current_page > all_pager:
current_page = all_pager
self.current_page = current_page
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)
【3】CommonVerifyCode : 图形验证码
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import random
from io import BytesIO
def 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.POST
username = 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.py
def 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 }}" disabled
class="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=' + txt
window.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 template
register = 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)
<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)
# 拼接并返回图片url
url = 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 BeautifulSoup
def __analysis_content(content):
# 解析HTML
soup = 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 = 1
elif 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.user
if not user_obj.is_authenticated:
return CommonResponse(code=400, errors='请先登录')
# 获取ajax发送的数据
data = request.POST
article_id = int(data.get('article_id'))
is_up = True if data.get('tag') == 'true' else False
state = '点赞' 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_up
up_down_obj.save()
if is_up:
article_obj.up_count += 1
article_obj.down_count -= 1
else:
article_obj.up_count -= 1
article_obj.down_count += 1
else:
# 如果原表中没有值,新增值
UpOrDown.objects.create(user=user_obj, article=article_obj, is_up=is_up)
if is_up:
article_obj.up_count += 1
else:
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.py
from 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,如果有主评论,那么设置值,如果没有默认就是null
let 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');
// 回复时设置主评论id
parentId = 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
获得
- 基本上每一个试图函数都需要使用的参数是